Skip to content
Theory · Asynchronous code · lesson 2 of 7

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.

LiveTwo awaits in turnstep 1 of 8 · frames: 1

Before the first await, the code runs as usual and prints order.

Watch where main waits and which line brew carries on from

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:

SWIFT
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:

  1. brewLatte(milk:) is declared async throws: it waits and may throw an error.
  2. The call brewLatte(milk: 0) first waits 100 ms on Task.sleep.
  3. Then guard sees that there is no milk, and the function throws CafeError.outOfMilk.
  4. The error comes back to the line with try await, the print inside do does not run, and control moves to catch.
  5. catch prints no 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 throw and caught in catch, 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 await a 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 return and throws an error with throw; you catch the error in a catch around try await.