SWIFT

Swift Async Await: Syntax, Usage, and Practical Examples

The Swift async await syntax allows you to write asynchronous code in a more readable and structured way. Introduced in Swift 5.5, this feature streamlines asynchronous programming by replacing traditional callback-based models with a cleaner, more linear syntax. When you use async await Swift, your asynchronous code behaves more like synchronous code, making it easier to reason about and debug.

Asynchronous programming is crucial in modern iOS, macOS, and server-side Swift apps, especially when performing tasks like networking, file I/O, database access, or background processing. The Swift async/await model ensures that your app remains responsive while offloading long-running tasks to background threads.


Why Use Async and Await in Swift?

Before Swift 5.5, developers relied heavily on completion handlers, delegate patterns, or reactive libraries to manage concurrency. These methods often led to deeply nested closures or complex callback chains, sometimes referred to as "callback hell." The async await Swift syntax eliminates these issues by allowing functions to pause and resume their execution naturally.

Using async and await in Swift improves code clarity and safety by making it easier to track the flow of asynchronous logic and error handling.


Declaring Async Functions in Swift

An async function is declared using the async keyword after the parameter list and before the return type.

func fetchUserData() async -> User {
    // Some asynchronous work
}

You can only call async functions from other async functions or from a Task. This ensures that you stay within a cooperative asynchronous execution context.


Using Await in Swift

The await keyword is used to pause the execution of an async function until the awaited task completes.

let user = await fetchUserData()

This statement looks synchronous but under the hood, Swift schedules the task and resumes execution once the result is available.


Example: Network Request with Async Await

func fetchPost() async throws -> Post {
    let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(Post.self, from: data)
}

In this example, URLSession.shared.data(from:) is an async method. The await keyword tells Swift to suspend execution until the network call returns.

You can then use this function like this:

Task {
    do {
        let post = try await fetchPost()
        print(post.title)
    } catch {
        print("Error: \(error)")
    }
}

Structured Concurrency

With the introduction of Swift async await, Apple also introduced structured concurrency, which provides a well-defined hierarchy for managing concurrent tasks. This approach helps avoid orphaned tasks and improves code predictability.

Using async let for Concurrent Work

You can run multiple async operations concurrently and await them later:

async let post = fetchPost()
async let comments = fetchComments()

let (loadedPost, loadedComments) = await (post, comments)

This lets you parallelize multiple asynchronous operations efficiently without threads or queues.


Using Tasks in Swift

The Task API is used to create an asynchronous context from synchronous code:

Task {
    let user = await fetchUserData()
    print(user.name)
}

This is useful when you're working in synchronous code but need to start an async process—like from a button tap or app launch event.

You can also control the priority and cancellation of a task using the Task API:

let task = Task(priority: .high) {
    await doSomething()
}

Throwing Async Functions

You can combine async and throws to create functions that perform asynchronous work and may fail:

func loadImage() async throws -> UIImage {
    let (data, _) = try await URLSession.shared.data(from: someURL)
    guard let image = UIImage(data: data) else {
        throw ImageError.invalidData
    }
    return image
}

Use try await when calling such functions:

let image = try await loadImage()

This makes error propagation as simple as it is in synchronous Swift code.


Bridging Legacy Code with Async

You can bridge existing callback-based APIs into the Swift async await world using withCheckedContinuation or withUnsafeContinuation.

func fetchValue() async -> Int {
    await withCheckedContinuation { continuation in
        legacyFetch { result in
            continuation.resume(returning: result)
        }
    }
}

This allows you to progressively migrate older codebases to the async/await model without needing full rewrites.


Cancellation in Async Await Swift

Swift supports cooperative cancellation in async contexts. Each task checks whether it has been canceled and responds accordingly.

func longRunningTask() async {
    for i in 0..<100 {
        if Task.isCancelled { return }
        print(i)
        await Task.sleep(1_000_000_000) // 1 second
    }
}

You can cancel a task like this:

let task = Task {
    await longRunningTask()
}

task.cancel()

This approach helps preserve battery life and system resources by halting unneeded work.


Common Use Cases for Async Await in Swift

  • Fetching data from APIs

    Replace URLSession completion handlers with clean, async code.

  • Image and asset loading

    Load large assets without blocking the UI.

  • Background computation

    Move expensive operations off the main thread while preserving structured flow.

  • Database access

    Interact with local or remote databases asynchronously, ensuring fast response times.

  • Authentication flows

    Chain multiple network-dependent steps like login, token refresh, and user data fetching using readable, linear logic.

These use cases benefit greatly from the readability and control that async await Swift provides.


SwiftUI and Async Await

You can use Task within SwiftUI views to perform async work when the view appears.

struct ContentView: View {
    @State private var user: User?

    var body: some View {
        VStack {
            if let user {
                Text(user.name)
            } else {
                ProgressView()
            }
        }
        .task {
            user = await fetchUserData()
        }
    }
}

This native support for async tasks in the view lifecycle makes SwiftUI apps more responsive and declarative.


Summary

The Swift async await model marks a major evolution in Swift’s concurrency system. It allows you to write asynchronous code that reads and behaves like synchronous code, while still leveraging the benefits of concurrent execution.

Using async await Swift, you gain better readability, error propagation, cancellation support, and task management. Combined with structured concurrency and task isolation, Swift’s modern concurrency tools offer powerful mechanisms to write clean, reliable, and performant applications. For developers familiar with traditional completion handlers or reactive programming, async/await provides a simpler and safer alternative aligned with the future direction of the Swift language.

Learn to Code in Swift for Free
Start learning now
button icon
To advance beyond this tutorial and learn Swift by doing, try the interactive experience of Mimo. Whether you're starting from scratch or brushing up your coding skills, Mimo helps you take your coding journey above and beyond.

Sign up or download Mimo from the App Store or Google Play to enhance your programming skills and prepare for a career in tech.

You can code, too.

© 2025 Mimo GmbH

Reach your coding goals faster