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

async/await vs. async let

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 of async 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.myProperty might not be safe in async let, unless you’re in an actor.

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

Featureawaitasync let
Starts task immediately?NoYes
Waits immediately?YesNo
Use when awaiting multiple values in parallel?No✅ Yes
Use when accessing self/mutable state?SaferRisky (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., reading self is restricted unless in actor).
  • The scope of the async let is the enclosing block — you must await 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 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

  • 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.”


📬 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.