3 mistakes to avoid with async / await


You’re more of a video kind of person? I’ve got you covered! Here’s a video with the same content than this article 🍿


I want to show you 3 mistakes you really want to avoid when using async / await in Swift!

#01 – Not running code concurrently when possible

Take a look at this code:

I’m making 3 separate asynchronous calls to retrieve various pieces of data about a User.

This code seems perfectly reasonable at first glance.

But if we look closer, we can notice that the 3 calls are made sequentially: the next call starts only after the previous one has finished.

While this makes sense for the first call, there’s actually no reason to run the two other calls sequentially.

Since they depend on the same argument, nothing prevents us from running them concurrently.

And it’s actually quite easy to do, we just need to use the async let syntax:

With this new syntax, both calls will immediately start executing concurrently.

You can notice that the keyword await has been removed.

It is normal, with this new syntax we will only await when we actually want to use the result of the asynchronous calls 👌

#02 – Not understanding that Task automatically captures self

Let’s switch to a different topic and talk about memory management!

At first glance this code looks totally harmless: we’re listening to system notifications and handling them as they are produced.

However, this code actually causes a memory leak!

The reason is that when we use self implicitly inside a Task, self gets captured by the Task and will stay in memory for as long as the Task is running.

Here the Task listens to an AsyncSequence that will never end, so the Task will never finish and the instance of ViewModel will never be released from memory.

This is especially tricky, because contrary to most @escaping closures, Swift doesn’t force us to explicitly capture self inside a Task

However it’s possible to solve the issue!

First, we need to capture a weak reference to self.

Then, we need to unwrap that weak reference.

However it’s very important that we do so inside the for loop!

First because it allows us to stop the Task when self is no longer in memory.

But also because if we had unwrapped self outside the loop, then the Task would still be holding on to a local strong reference to self, and it would lead to the exact same issue we’re trying to fix!

So you really want to be careful when you create a Task that could run forever, like the one in this example, because such a Task has the potential of creating a memory leak 😱

#03 – Using Task.detached when not needed

To finish, you might have noticed that there’s another way to create a Task: by using Task.detached.

You might even find on the Internet old tutorials that use Task.detached as a standard way of creating a Task.

This is most likely not what you want!

A detached Task is a Task that will run in its own context, regardless of where it was created.

To understand what it means, let’s go back to the previous example.

In this example, I was creating a Task inside a method of a class that is annotated with @MainActor.

This meant that my Task inherited that actor context, and could, for instance, easily manipulate objects that are bound to the Main thread.

However, if I change this to a Task.detached we can see some errors are popping up.

These errors are perfectly normal: since our Task now runs detached from the @MainActor context, we need to explicitly acknowledge the potential suspend point whenever we try to access something that is bound to the @MainActor and we do so by adding an await at each of these call sites.

This illustrates why most of the time you actually don’t want to use Task.detached: it only adds extra complexity without solving any issue.

Conclusion

That’s it, these were the 3 mistakes you definitely want to avoid when using async / await in Swift!

I hope you’ve enjoyed this article and that it will help you avoid some tricky pitfalls!


You’ve enjoyed this article and you want to support my content creation?

You can leave me a tip ☺️ 👇

Buy Me A Coffee

Here’s the code it you want to experiment with it!

import Foundation

// #01 – Not running code concurrently when possible

// This call will run first…

let user = await getUser()

// ...and after it has completed, the
// two others will then run concurrently

async let address = getAddress(of: user)

async let paymentMethod = getPaymentMethod(of: user)

await print("\(address) \(paymentMethod)”)

// #02 – Not understanding that `Task` automatically captures `self`

@MainActor
class ViewModel {
    func handle(_ notification : Notification) {
        // do something with the `notification`
    }

    func listenToNotifications() {
        Task { [weak self] in
            guard let self else { return }

            // Here the `Task` still holds a local
            // strong reference to `self` forever 😱

            let notifications = NotificationCenter.default.notifications(
                named: UIDevice.orientationDidChangeNotification
            )

            for await notification in notifications {
                self.handle(notification)
            }
        }
    }
}

// #03 – Using `Task.detached` when not needed

@MainActor
class ViewModel {
    func handle(_ notification : Notification) {
        // do something with the `notification`
    }

    func listenToNotifications() {
        Task.detached { [weak self] in
            let notifications = await NotificationCenter.default.notifications(
                named: UIDevice.orientationDidChangeNotification
            )

            for await notification in notifications {
                guard let self else { return }

                await self.handle(notification)
            }
        }
    }
}
Previous
Previous

I learned so much from these videos 😌

Next
Next

Do you know this trick to spot memory leaks? 🤨