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
- • What async and await mean (suspend and resume)
- • Calling async code from sync code with Task { }
- • Sequential awaits vs concurrent async let
- • Running independent work in parallel for speed
- • Protecting shared mutable state with actors
- • Running UI code on the main thread with @MainActor
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: 42Pro Tips
- 💡 Use async let whenever requests don't depend on each other — it's free speed.
- 💡 Update UI on the main actor. Mark UI code @MainActor or hop with await MainActor.run { }.
- 💡 Prefer actors over manual locks for shared mutable state — they're race-free by design.
- 💡 One await per suspension. Add try too when the call can throw: try await.
Common Errors (and the fix)
- "'async' call in a function that does not support concurrency": you called async code from sync code. Wrap it in Task { } or mark the caller async.
- "Expression is 'async' but is not marked with 'await'": add await before the call.
- Awaiting calls one-by-one when they're independent: switch to async let to run them in parallel.
- UI updated off the main thread: wrap it with @MainActor / await MainActor.run { }.
📋 Quick Reference
| Task | Code | Effect |
|---|---|---|
| Define async | func f() async { } | may suspend |
| Call it | await f() | suspend point |
| From sync code | Task { await f() } | async context |
| Parallel | async let x = f() | runs concurrently |
| Safe state | actor C { … } | no data races |
| Main thread | @MainActor | UI-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
- Previous: Memory Management & ARC
- Next: Codable & JSON — Encode and decode JSON effortlessly with Codable and CodingKeys
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.