SWIFT

Swift Async Await: Syntax, Usage, and Practical Examples

The async/await syntax in Swift allows you to write asynchronous code in a more readable and structured way. Introduced in Swift 5.5, it replaces traditional callback-based models with a cleaner, more linear syntax. As a result, your code behaves more like synchronous code, making it easier to understand and debug.

Asynchronous programming is essential in modern iOS, macOS, and server-side Swift apps—especially for networking, file I/O, database access, and background processing. This new model ensures your app remains responsive by offloading long-running tasks to background threads.


Why Use Async and Await?

Before Swift 5.5, developers relied on completion handlers, delegate patterns, or reactive libraries to handle concurrency. These methods often led to deeply nested closures and hard-to-follow callback chains, commonly referred to as "callback hell". Async/await eliminates these issues by allowing functions to pause and resume naturally.

This model improves code clarity, reduces boilerplate, and simplifies error handling in asynchronous flows.


Declaring Async Functions

You define an asynchronous function using the async keyword after the parameter list:

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

These functions must be called from within another async context or a Task.


Using Await in Practice

Use the await keyword to pause execution until an async operation completes:

let user = await fetchUserData()

Though it looks synchronous, Swift manages the suspension and resumption under the hood.


Example: Network Request

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)
}

This async function retrieves data from a remote server. You can call it inside a Task like this:

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

Structured Concurrency

Swift’s modern concurrency system introduces structured concurrency, which organizes tasks into predictable hierarchies. This helps avoid issues like orphaned tasks and improves reliability.

Running Multiple Tasks Concurrently

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

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

By using async let, you can launch operations in parallel and await their results later, making your code faster and more efficient.


Using Tasks to Start Async Work

The Task API lets you start asynchronous operations from synchronous contexts, like UI event handlers:

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

You can also manage priority and cancellation:

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

Combining Async and Throws

You can define functions that both throw errors and perform asynchronous work:

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
}

When calling such functions, use try await:

let image = try await loadImage()

This simplifies error propagation while keeping the logic clean and readable.


Adapting Legacy Code

You can convert callback-based APIs to async by using withCheckedContinuation:

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

This makes it possible to gradually adopt async/await without rewriting your entire codebase.


Handling Cancellation

Swift supports cooperative cancellation. Tasks can check whether they’ve been canceled and stop accordingly:

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

To cancel a task:

let task = Task {
    await longRunningTask()
}

task.cancel()

This helps conserve system resources and avoid unnecessary processing.


Common Use Cases

Async/await in Swift is useful for:

  • API requests — Replace nested completion handlers with cleaner async calls.
  • Image and asset loading — Prevent UI blocking by loading in the background.
  • Heavy computation — Offload intensive work from the main thread.
  • Database interactions — Ensure responsiveness when working with data sources.
  • Authentication flows — Sequence dependent steps like login and user data retrieval.

These scenarios benefit from async/await’s readability and structured approach.


SwiftUI Integration

SwiftUI supports async workflows via .task modifiers:

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 enables you to load data as soon as a view appears, keeping your UI responsive and declarative.


Summary

Async/await in Swift marks a major improvement to how concurrency is handled. It enables you to write asynchronous code that looks and behaves like synchronous code—without the complexity of callbacks or external libraries.

By using async, await, structured concurrency, and Task, you gain tools for clearer logic, better error handling, and efficient task execution. As Swift continues to evolve, async/await is becoming the go-to approach for writing reliable and modern asynchronous applications.

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