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 ☺️ 👇
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)
}
}
}
}