async and await
A customer with a buzzer can place two orders in a row. First they order tea, wait for the signal and pick up the cup, and only then order coffee. The second order starts when the first is ready. Two awaits in a row behave the same way.
Awaits go in turn
await waits until the call ends, and only then does the code move on to the next line. So two calls with await in a row run in turn: the second starts when the first has returned its result.
Let's see it in the widget. The function brew makes a drink: it prints that it has started, waits for a pause and prints that the drink is ready. The length of the pause, 50 ms, is kept in the constant pause of type Duration. Press "Next step" and watch the column of calls: at each await the main box waits, and after the pause brew carries on from the next line.
1let pause: Duration = .milliseconds(50)2 3func brew(_ drink: String) async throws {4 print("brewing \(drink)")5 try await Task.sleep(for: pause)6 print("\(drink) ready")7}8 9print("order")10try await brew("tea")11try await brew("coffee")12print("done")
Before the first await, the code runs as usual and prints order.
The main thing in the widget: the call brew("coffee") started only when brew("tea") had ended. So the pauses added up: two orders of 50 ms each took at least 100 ms.
Interviewers like to ask this: what runs at the same time in several awaits in a row? The answer is nothing. Each call waits until the one before it ends.
Errors and try await
An async function throws an error the same way an ordinary one does, with throw. The error travels through await to whoever waits for the result, and you catch it in do/catch, as in the errors theme:
enum CafeError: Error {
case outOfMilk
}
func brewLatte(milk: Int) async throws -> String {
try await Task.sleep(for: .milliseconds(100))
guard milk > 0 else { throw CafeError.outOfMilk }
return "latte"
}
do {
print(try await brewLatte(milk: 0))
} catch {
print("no latte:", error) // no latte: outOfMilk
}Let's see what happens:
brewLatte(milk:)is declaredasync throws: it waits and may throw an error.- The call
brewLatte(milk: 0)first waits 100 ms onTask.sleep. - Then
guardsees that there is no milk, and the function throwsCafeError.outOfMilk. - The error comes back to the line with
try await, theprintinsidedodoes not run, and control moves tocatch. catchprintsno latte: outOfMilk.
The result and the error come back through the familiar return and throw, even though a pause passed between the call and the answer.
How waiting worked before
Before async/await, waiting was built on closures. A function took a completion handler: a closure it would call once the result was ready. The function itself returned at once, still without a result. To wait for two results in turn, you wrote the second call inside the first one's handler, so the closures nested one inside another.
How async/await beats a completion handler is a common interview question. The answer:
- the code reads from top to bottom instead of going ever deeper into nested closures;
- the result comes back with
return, not as a closure parameter; - an error is thrown with
throwand caught incatch, as in ordinary code; - the wait still does not block the program, as before.
Key points
- Two
awaits in a row run in turn: the next call starts when the one before it has ended. - At
awaita function pauses, and then carries on from the next line with all its variables. - Unlike a completion handler, an async function returns its result with
returnand throws an error withthrow; you catch the error in acatcharoundtry await.