Swift async/await vs. async let — A Practical Cheat Sheet

Swift concurrency introduces async/await and async let for handling asynchronous code – but when should you use which? This cheat sheet breaks down the key differences with examples, best practices, and real-world use cases.
While exploring Swift’s concurrency model, I worked with ChatGPT to create a clear Q&A around the differences between await and async let. The outcome is this practical cheat sheet – a quick reference to understand when to use await for sequential async calls and when async let is the better choice for concurrent tasks in Swift.
🧩 Summary
- Use
awaitinstead ofasync letif you’re awaiting only one thing. - Swift protects against unsafe access to
selfand its mutable properties across concurrency boundaries (quite extensive starting with Swift 6) - Even reading from
self.myPropertymight not be safe inasync let, unless you’re in anactor.
await: Structured sequential concurrency
You use await when you want to wait for the result of an async function before moving on.
✅ Use when:
- You want one task to complete before starting the next.
- You’re only awaiting a single operation.
💡 Example:
let first = await fetchFirstValue()
let second = await fetchSecondValue()
⏱ In this case, fetchSecondValue() starts only after fetchFirstValue() completes.
async let: Structured concurrent concurrency
You use async let when you want to start multiple async tasks in parallel and then await their results later.
✅ Use when:
- You want to start multiple async operations at once.
- You don’t need the result immediately.
- You want to wait for all results later, once they’ve started in parallel.
💡 Example:
async let first = fetchFirstValue()
async let second = fetchSecondValue()
let (a, b) = await (first, second)
⏱ Now both fetchFirstValue() and fetchSecondValue() start at the same time, and you await their results later. This can be much faster.
🧠 Key Differences
| Feature | await | async let |
|---|---|---|
| Starts task immediately? | No | Yes |
| Waits immediately? | Yes | No |
| Use when awaiting multiple values in parallel? | No | ✅ Yes |
Use when accessing self/mutable state? | Safer | Risky (needs care!) |
⚠️ Caution with async let
- It starts child tasks immediately.
- Swift limits what you can do inside
async letdeclarations to prevent data races (e.g., readingselfis restricted unless inactor). - The scope of the
async letis the enclosing block — you mustawaitits result before the end of the function.
✅ TL;DR
- Use
awaitfor simple sequential async calls. - Use
async letto run multiple async calls concurrently and then wait for them together — for performance.
🧪 Case: Fetch multiple sensor readings from cloud
Imagine you need to fetch temperature, humidity, and battery level – each via an async API call.
👇 Using await (sequential)
let temperature = await dataProvider.getTemperature(for: deviceId)
let humidity = await dataProvider.getHumidity(for: deviceId)
let battery = await dataProvider.getBatteryLevel(for: deviceId)
⏱ Timeline:
|--- getTemperature ---|--- getHumidity ---|--- getBattery ---|
✅ Safe, but slow – each request starts only after the previous one finishes.
👇 Using async let (concurrent)
async let temperature = dataProvider.getTemperature(for: deviceId)
async let humidity = dataProvider.getHumidity(for: deviceId)
async let battery = dataProvider.getBatteryLevel(for: deviceId)
let (t, h, b) = await (temperature, humidity, battery)
⏱ Timeline:
|--- getTemperature ---|
|--- getHumidity ------|
|--- getBattery -------|
✅ Fast – all requests start at the same time and run concurrently. You only wait once, after they’ve all started.
⚠️ Why async let can trigger “Data Race” Warnings
In your class, when you write:
async let latestValues = dataProvider.getSensorLatestTimeseries(for: self.deviceId, ...)
You’re capturing self (or deviceId, which is part of self) in an async task that runs concurrently, which Swift interprets as a potential data race, unless you’re in an actor or isolate it.
✅ Fix / Safe version
To avoid the warning:
let id = self.deviceId // local copy, no race risk
async let latestValues = dataProvider.getSensorLatestTimeseries(for: id, ...)
By copying out deviceId, you avoid passing self into the async closure.
🧠 Real-World Analogy
awaitis like: “Get coffee, then toast, then juice – one by one.”async letis like: “Ask three people to each prepare one item at the same time, and wait until all are ready.”
📬 Stay in the loop?
If you’d like to stay up to date with future technical guides and project insights, subscribe to my newsletter. I share practical knowledge, lessons learned, and updates – no spam, just content for developers and engineers.