Concurrency: async/await

Reviewed & published by Brayan K

Write asynchronous code that reads like ordinary top-to-bottom code. Learn async/await, launch work with Task, run things in parallel with async let, and keep shared state safe with actors and the @MainActor.

Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ async / await Basics

An async function can suspend; await marks where it pauses. From ordinary code, wrap async calls in a Task { } to create an async context.

// 'async' marks a function that can PAUSE; 'await' marks where it pauses.
// Pausing frees the thread — the app stays responsive while it waits.

func fetchUsername() async -> String {
    // (Imagine a slow network call here.)
    return "Ada"
}

func greet() async {
    let name = await fetchUsername()   // suspend here until it returns
    print("Hello, \(name)!")          // resumes with the result
}

// Call an async function from ordinary code by creating a Task.
Task {
    await greet()                       // Hello, Ada!
}

2️⃣ Sequential awaits

Plain await calls run one after another — each waits for the previous to finish. That's correct when later work depends on earlier results.

// Sequential awaits run ONE AFTER ANOTHER — each waits for the previous.
func loadNumber(_ n: Int) async -> Int {
    return n * 10
}

func sumSequential() async {
    let a = await loadNumber(1)   // finishes, THEN...
    let b = await loadNumber(2)   // ...this starts
    let c = await loadNumber(3)
    print("Sum = \(a + b + c)")   // Sum = 60
}

Task {
    await sumSequential()
}

3️⃣ Concurrency with async let

When operations are independent, async let starts them all at once so they overlap. You await the results later — total time is roughly the longest single task, not the sum.

// 'async let' starts work CONCURRENTLY. The three calls below kick off
// immediately and run in parallel; we 'await' the results together.

func loadNumber(_ n: Int) async -> Int {
    return n * 10
}

func sumConcurrent() async {
    async let a = loadNumber(1)   // all three start...
    async let b = loadNumber(2)   // ...at the same time
    async let c = loadNumber(3)

    // Now await them. Independent work overlaps instead of stacking up.
    let total = await a + b + c
    print("Sum = \(total)")       // Sum = 60
}

Task {
    await sumConcurrent()
}

4️⃣ Actors and @MainActor

An actor serializes access to its mutable state, preventing data races. Reaching into an actor from outside is asynchronous, so it needs await. The special @MainActor pins code to the main thread — essential for UI updates.

// An ACTOR protects its mutable state: only one task touches it at a time,
// so you can't get a data race. Access from OUTSIDE is async (needs await).

actor Counter {
    private var value = 0
    func increment() { value += 1 }
    func current() -> Int { value }
}

let counter = Counter()

Task {
    await counter.increment()     // cross-actor call -> await
    await counter.increment()
    let n = await counter.current()
    print("Counter = \(n)")       // Counter = 2
}

// @MainActor pins code to the main thread — required for UI updates.
@MainActor
func updateUI(_ text: String) {
    print("UI shows: \(text)")
}

Your turn. Fill in the async keyword, the suspend keyword, and the Task type.

// 🎯 YOUR TURN — fill in each ___ then run it on SwiftFiddle.

func slowDouble(_ n: Int) ___ -> Int {   // 👉 the keyword that allows suspension
    return n * 2
}

func run() async {
    // 1) Suspend until slowDouble returns.
    let x = ___ slowDouble(21)            // 👉 the suspend keyword
    print("Result: \(x)")                // Result: 42
}

// 2) Call an async function from sync code with a Task.
___ {                                     // 👉 the type that creates an async context
    await run()
}

// ✅ Expected output:
//    Result: 42

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCodeEffect
Define asyncfunc f() async { }may suspend
Call itawait f()suspend point
From sync codeTask { await f() }async context
Parallelasync let x = f()runs concurrently
Safe stateactor C { … }no data races
Main thread@MainActorUI-safe

Mini-Challenge: Parallel Loaders

No blanks this time — just a brief and an outline. Load three things concurrently with async let and print them together.

// 🎯 MINI-CHALLENGE: parallel loaders
// 1. Write  func load(_ label: String) async -> String
//    that returns "loaded <label>".
// 2. In an async function, use THREE 'async let' bindings to load
//    "users", "posts", and "stats" concurrently.
// 3. await all three and print them on one line.
// 4. Kick it off from a Task { }.
//
// ✅ Expected output:
//    loaded users | loaded posts | loaded stats

// your code here

Practice quiz

What does marking a function 'async' mean?

  • It runs on the GPU
  • It always runs in parallel
  • It can suspend and resume — pausing without blocking the thread
  • It cannot fail

Answer: It can suspend and resume — pausing without blocking the thread. 'async' marks a function that may suspend at await points, freeing the thread while it waits.

What does the 'await' keyword do?

  • Marks a point where the function may suspend until the async result is ready
  • Blocks the thread until done
  • Starts a new thread
  • Catches an error

Answer: Marks a point where the function may suspend until the async result is ready. 'await' marks a potential suspension point where execution can pause and later resume.

How do you call an async function from non-async (synchronous) code?

  • Just call it directly
  • Use a for loop
  • It is impossible
  • Wrap it in a Task { }

Answer: Wrap it in a Task { }. A Task { } creates an async context so you can call and await async functions from sync code.

What is 'async let' used for?

  • Declaring a constant
  • Starting child tasks that run concurrently, awaited later
  • Looping asynchronously
  • Defining an actor

Answer: Starting child tasks that run concurrently, awaited later. 'async let' kicks off concurrent work immediately and you await the results when you need them.

Compared to sequential awaits, what does 'async let' improve?

  • It lets independent operations run in parallel, reducing total wait time
  • Memory usage only
  • Code safety only
  • Nothing

Answer: It lets independent operations run in parallel, reducing total wait time. Independent async let bindings run concurrently instead of one-after-another, so the total time is roughly the longest one, not the sum.

What problem do actors primarily solve?

  • Slow networking
  • JSON decoding
  • Protecting mutable state from data races by serializing access
  • UI layout

Answer: Protecting mutable state from data races by serializing access. An actor isolates its mutable state so only one task touches it at a time, preventing data races.

Accessing an actor's property or method from outside the actor usually requires what?

  • A force-unwrap
  • await, because the access may suspend
  • A guard statement
  • Nothing special

Answer: await, because the access may suspend. Cross-actor access is asynchronous, so you must await it.

What does the @MainActor attribute guarantee?

  • The code runs on a background thread
  • The code never suspends
  • The code is private
  • The code runs on the main thread/actor — required for UI updates

Answer: The code runs on the main thread/actor — required for UI updates. @MainActor ensures the annotated code runs on the main actor, which is where UI work must happen.

Inside an async function, how do you await two independent calls so they overlap?

  • await a(); await b()
  • async let x = a(); async let y = b(); let r = await (x, y)
  • Task { a() }; Task { b() }
  • guard let

Answer: async let x = a(); async let y = b(); let r = await (x, y). async let starts both concurrently; awaiting the tuple lets them overlap rather than run sequentially.

Why is async/await preferred over nested completion-handler callbacks?

  • It is the only option
  • It is faster on the CPU
  • It reads top-to-bottom like synchronous code, avoiding callback nesting
  • It removes the need for error handling

Answer: It reads top-to-bottom like synchronous code, avoiding callback nesting. async/await flattens asynchronous logic into straight-line, readable code instead of pyramids of callbacks.

Continue this course

Frequently asked questions

What do async and await actually mean?

Marking a function 'async' declares that it can suspend — pause partway through and resume later — without blocking the thread it started on. The 'await' keyword marks the exact points where that suspension may happen, usually a call to another async function. While suspended, the thread is free to do other work, which is why async/await keeps apps responsive. Together they let you write asynchronous code that reads top-to-bottom like ordinary synchronous code.

How do I call an async function from regular synchronous code?

You can't await directly in a non-async context, so you create a Task. Writing Task { await someAsyncCall() } sets up an asynchronous context that runs the work and lets you use await inside it. In SwiftUI you often use the .task modifier, which creates a Task tied to a view's lifetime. From an async function you simply call other async functions with await directly.

What is the difference between sequential await and async let?

Plain sequential 'await' calls run one after another: the second can't start until the first finishes. 'async let' starts each piece of work immediately and concurrently, then lets you await the results later. When the operations are independent — say three separate network requests — async let runs them in parallel so the total time is about the longest single request rather than the sum of all of them. Use it whenever the work doesn't depend on earlier results.

What is an actor and what problem does it solve?

An actor is a reference type that protects its own mutable state from data races. It guarantees that only one task at a time can access its mutable properties and methods, serializing access automatically so you don't need manual locks. Because reaching into an actor from outside may have to wait its turn, cross-actor access is asynchronous and you must use await. Actors are Swift's safe, built-in answer to shared mutable state in concurrent code.

What does @MainActor do and when do I need it?

@MainActor marks a function, property, or type so that its code always runs on the main actor — effectively the main thread. UIKit and SwiftUI require UI updates to happen on the main thread, so you annotate UI-touching code with @MainActor (or hop onto it with await MainActor.run { ... }) to guarantee that. SwiftUI's View bodies and .task closures already run on the main actor, so assigning to @State there is safe without extra annotation.