100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Swift

async/await Basics in Swift

Learn how Swift's async/await syntax replaces completion-handler callbacks with linear, readable asynchronous code, and how to call async functions safely.

Concurrency & CombineIntermediate9 min readJul 8, 2026
Analogies

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.

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

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

  • async marks a function as capable of suspending; await marks a call site as a potential suspension point.
  • await can only be used inside an async context, which the compiler enforces at compile time.
  • Sequential await calls run one after another; use async let or task groups to run independent work concurrently.
  • async functions can be combined with throws for error propagation using ordinary try/catch.
  • An async function does not guarantee execution on a background thread — it only enables suspension at await points.
  • SwiftUI's .task modifier provides a built-in async context for launching async work tied to a view's lifecycle.

Practice what you learned

Was this page helpful?

Topics covered

#Swift#IOSWithSwiftUIStudyNotes#MobileDevelopment#AsyncAwaitBasicsInSwift#Async#Await#Declaring#Calling#Concurrency#StudyNotes#SkillVeris