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

Waiting without blocking

What you'll learn

What you'll learn:

  • explain how waiting with await differs from waiting that stops the program;
  • declare async functions and call them with await and try await;
  • run two jobs at once with async let, and a separate task with Task;
  • protect shared data with an actor and explain what a data race is.
A customer waits at a table with a buzzer await while the cashier under the sign async serves the next one

Picture a café without buzzers. You order a latte and stand at the counter while the barista makes it. The line behind you stands too: the cashier won't take the next order until you get yours.

Now the same café with buzzers. A sign over the counter says orders are handed out on a signal. You order, take a buzzer and step aside to a table, and the cashier serves the next customer right away. The buzzer goes off, you pick up your cup and carry on from where you stopped.

Programs have to wait too: for a server's reply, for a file to be read, for a timer's signal. There are two ways to wait:

  • with blocking, like at a counter without buzzers: until the answer comes, this part of the program stands still and does nothing else;
  • without blocking, like with a buzzer: until the answer comes, the program can do other work.

Why does it matter? If a phone app waits for a server's reply with blocking, it freezes: the screen won't scroll and the buttons won't respond. Without blocking, the app keeps drawing the screen and responding to taps while the reply is still on its way.

In Swift, the sign over the counter is the word async in a function's declaration, and the buzzer in your hand is the word await where you call it. Let's see them in code:

SWIFT
func makeCoffee() async throws -> String {
    try await Task.sleep(for: .milliseconds(100))
    return "coffee"
}

print("order") // order
let drink = try await makeCoffee()
print(drink) // coffee

Async function

An async function is a function that can pause in the middle of its work while it waits for something, and then carry on. You mark it with the word async in its header, after the parentheses with the parameters.

Let's walk through the program step by step:

  1. func makeCoffee() async throws -> String declares an async function. The word async comes after the parameters, before throws and the arrow.
  2. Inside it, Task.sleep(for: .milliseconds(100)) waits 100 milliseconds without blocking the program. In a real app, a request to a server would be here; the pause stands in for it.
  3. print("order") prints order.
  4. try await makeCoffee() calls the function and waits for its result. When the pause is over, drink gets the string "coffee".
  5. print(drink) prints coffee.

await and the suspension point

You write await in front of every call to an async function. It marks a suspension point: a place where the code may stop and wait. While the code waits, Swift gives the freed time to other work, like a cashier who serves the next customer.

When the result is ready, the code carries on from the same place. All the function's variables are still there, so it carries on as if there had been no pause.

You can write await where pausing is allowed:

  • inside an async function;
  • right in main.swift outside any function, like try await makeCoffee() in the example.

In an ordinary function, without async, await does not compile: that function is not allowed to pause.

Here the buzzer picture stops working. A customer at the table is free to make a call or read. A function at await does nothing: it only waits, and the rest of the program does the other work.

The pause and try

Task.sleep is declared with throws: a wait can be cancelled, and then it throws an error. So:

  • you call Task.sleep with try await;
  • the function makeCoffee, which calls it with try, is itself declared with throws;
  • the word order never changes: async throws in the declaration, try await in the call.

As you already know, in the main part of the program, that is, in main.swift outside any function, try works without do/catch too. If nobody catches the error, the program crashes.

Common mistake

An async function was called without await:

SWIFT
let second = try makeCoffee() // ✗
TEXT
error: expression is 'async' but is not marked with 'await'

The program does not compile. A call to an async function can pause the code, and Swift wants that place to be visible in the program text. The fix is try await makeCoffee().

Key points

  • Waiting without blocking leaves the program time for other work while the answer is on its way.
  • async in the declaration makes a function async, and await at the call marks a suspension point.
  • You write await inside an async function and in main.swift outside any function. If the function also throws errors, the order is async throws in the declaration and try await in the call.