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
await
instead ofasync let
if you’re awaiting only one thing. - Swift protects against unsafe access to
self
and its mutable properties across concurrency boundaries (quite extensive starting with Swift 6) - Even reading from
self.deviceId
might 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 let
declarations to prevent data races (e.g., readingself
is restricted unless inactor
). - The scope of the
async let
is the enclosing block — you mustawait
its result before the end of the function.
✅ TL;DR
- Use
await
for simple sequential async calls. - Use
async let
to 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 sensor struct, 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
await
is like: “Get coffee, then toast, then juice – one by one.”async let
is like: “Ask three people to each prepare one item at the same time, and wait until all are ready.”
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.