async/await Basics in Swift
Swift's async/await is a language feature that lets asynchronous code be written and read like ordinary sequential code, without the deeply nested completion-handler closures that previously plagued networking and I/O code. A function marked async can suspend its execution at well-defined points — marked by the await keyword — while it waits for a long-running operation to finish, and the calling code resumes exactly where it left off once the value is ready, all without blocking the underlying thread. This is fundamentally different from DispatchQueue-based callbacks: control flow, error handling with try/catch, and even loops read naturally instead of being scattered across nested closures.
Cricket analogy: Instead of a captain shouting instructions through five relay fielders like nested closures, await lets the umpire simply pause play for a DRS review and resume exactly where the over left off, no confusion.
Declaring and Calling async Functions
A function becomes asynchronous by adding the async keyword to its signature, typically before throws if the function can also fail. Calling an async function requires the await keyword, which marks a potential suspension point — the compiler will not let you call an async function without it. Crucially, await can only appear inside another async context (an async function, a Task, or certain SwiftUI modifiers like .task), which is what makes the async/sync boundary explicit and checkable at compile time.
Cricket analogy: Marking a bowler's over as DRS-eligible (async) means any batsman requesting a review (await) must do so within that over's rules, not from the pavilion.
struct WeatherService {
func fetchTemperature(for city: String) async throws -> Double {
let url = URL(string: "https://api.example.com/weather?city=\(city)")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
throw URLError(.badServerResponse)
}
let decoded = try JSONDecoder().decode(WeatherReading.self, from: data)
return decoded.temperatureCelsius
}
}
struct WeatherView: View {
@State private var temperature: Double?
let service = WeatherService()
var body: some View {
Text(temperature.map { "\($0)°C" } ?? "Loading…")
.task {
do {
temperature = try await service.fetchTemperature(for: "Austin")
} catch {
print("Failed to fetch weather: \(error)")
}
}
}
}Sequential Awaits vs. Concurrent Work
Writing two await calls one after another executes them sequentially — the second does not start until the first completes. If the two operations are independent and you want them to run concurrently, you should use async let bindings or a task group instead, both of which start the work immediately and let you await the results later. Understanding this distinction is essential: naively awaiting in sequence when operations could run in parallel is a common source of unnecessary latency in SwiftUI apps.
Cricket analogy: Awaiting two bowlers one after another is like scheduling Bumrah's over then only starting Shami's after it ends; using async let is like running two nets sessions on parallel pitches simultaneously.
func loadDashboard() async throws -> (Double, [Order]) {
async let temperature = weatherService.fetchTemperature(for: "Austin")
async let orders = orderService.fetchRecentOrders()
return try await (temperature, orders)
}A helpful analogy: await is like ordering food at a counter and stepping aside to let the next customer be served while you wait for your order number to be called — the thread (the cashier) is freed to do other work, and you (the calling code) simply resume when your result is ready, rather than the cashier standing idle in front of you the whole time.
An async function does not automatically run on a background thread — it may suspend and resume on different threads managed by the cooperative thread pool, but if the function's body never actually awaits anything, it executes synchronously on the caller's thread just like any normal function. async alone does not guarantee concurrency; it only enables suspension.
asyncmarks a function as capable of suspending;awaitmarks a call site as a potential suspension point.awaitcan only be used inside an async context, which the compiler enforces at compile time.- Sequential
awaitcalls run one after another; useasync letor task groups to run independent work concurrently. asyncfunctions can be combined withthrowsfor error propagation using ordinarytry/catch.- An
asyncfunction does not guarantee execution on a background thread — it only enables suspension at await points. - SwiftUI's
.taskmodifier provides a built-in async context for launching async work tied to a view's lifecycle.
Practice what you learned
1. What does the `await` keyword indicate at a call site?
2. If you write `let a = try await fetchA(); let b = try await fetchB()` where `fetchA` and `fetchB` are independent, what happens?
3. How can you run two independent async operations concurrently in Swift?
4. Is it true that marking a function `async` automatically executes its body on a background thread?
5. Where can the `await` keyword legally appear?
Was this page helpful?
You May Also Like
Task and Structured Concurrency
Explore how Swift's Task type creates units of asynchronous work, how structured concurrency establishes parent-child relationships, and how cancellation propagates.
Networking with URLSession
Understand how URLSession performs HTTP requests in Swift, including async/await APIs, request configuration, and handling responses safely.
Publishers and @MainActor
Learn how to safely deliver Combine publisher values to the main thread using @MainActor and .receive(on:), and how actor isolation interacts with reactive pipelines.
Combine Framework Fundamentals
Understand Combine's publisher-subscriber model for handling asynchronous event streams in Swift, and how it relates to async/await and SwiftUI.