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