Getting Started with `async`/`await` in Swift


Imagine Woody trying to call Buzz on a walkie-talkie and being completely frozen — unable to move, talk, or do anything else — until Buzz finally picks up. That’s what happens when your app makes a network call without asynchronous code: the entire interface locks up waiting for a response.

Swift’s async/await lets you write code that waits for slow operations (like fetching data from the internet) without freezing your app. You’ll learn how to call async functions, write your own, and use Task to bridge into async code from SwiftUI. We won’t cover TaskGroup or actors — those get their own dedicated posts.

Apple Docs: Swift Concurrency — The Swift Programming Language

This guide assumes you’re comfortable with closures, error handling, and networking basics.

What You’ll Learn

What Is async/await?

Asynchronous means “doesn’t happen instantly.” When your app fetches a movie poster from the internet, that request takes time — maybe a few milliseconds, maybe a few seconds. An async function is a function that can pause while waiting for a slow operation, letting your app keep running in the meantime.

await is the keyword you use to say “pause here until the result comes back.” Think of it like Woody leaving a message at Sunnyside Daycare and going about his day — when the answer arrives, he picks up right where he left off.

Before async/await, Swift developers used completion handlers (closures that get called when work finishes). They worked, but led to deeply nested code that was hard to read. async/await lets you write asynchronous code that reads like normal, straight-line code.

Calling an Async Function

The simplest way to use async/await is to call a function someone else already marked as async. Many Apple frameworks now provide async versions of their APIs.

func fetchToyName() async -> String {
    // Simulates a network delay
    try? await Task.sleep(for: .seconds(1))
    return "Buzz Lightyear"
}

To call this function, you use the await keyword:

let name = await fetchToyName()
print("Fetched: \(name)")
Fetched: Buzz Lightyear

The await keyword marks the point where your code pauses. While it’s paused, your app isn’t frozen — other work (like animations and user interactions) continues normally.

Tip: You can only use await inside a context that is already async — like an async function or a Task block. The compiler will tell you if you try to use it in the wrong place.

Writing Your Own Async Functions

To make your own function asynchronous, add the async keyword after the parameter list and before the return type. If the function can also throw errors, the order is async throws.

func fetchMovie(id: Int) async throws -> String {
    let url = URL(string: "https://example.com/movies/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    let name = String(decoding: data, as: UTF8.self)
    return name
}

This function does three things:

  1. Creates a URL from the movie ID.
  2. Uses URLSession to fetch data from the network — this is the slow part, so we await it.
  3. Converts the raw data into a String and returns it.

Because the network call might fail, the function is marked async throws, meaning the caller needs both await and try.

do {
    let movie = try await fetchMovie(id: 1)
    print(movie)
} catch {
    print("Failed to fetch movie: \(error)")
}

Using Task in SwiftUI

SwiftUI views are not async by default, so you can’t just write await inside a body property. Instead, you use a Task to create an asynchronous context.

Apple Docs: Task — Swift Standard Library

import SwiftUI

struct MovieView: View {
    @State private var movieName = "Loading..."

    var body: some View {
        Text(movieName)
            .task {
                movieName = await fetchToyName()
            }
    }
}
Loading...
// After ~1 second:
Buzz Lightyear

The .task modifier is the recommended way to run async work when a view appears. It automatically cancels the work if the view disappears — like Woody’s walkie-talkie conversation ending when he leaves the room.

You can also create a Task manually inside a button action:

Button("Find Toy") {
    Task {
        movieName = await fetchToyName()
    }
}

Tip: Prefer .task over Task { } in onAppear when the async work is tied to the view’s lifecycle. The .task modifier handles cancellation for you.

Handling Errors in Async Code

When an async function is marked with throws, you handle errors with the same do/catch pattern you already know from error handling — just add await next to try.

struct PixarMovieView: View {
    @State private var title = ""
    @State private var errorMessage: String?

    var body: some View {
        VStack {
            Text(title)
            if let errorMessage {
                Text(errorMessage).foregroundStyle(.red)
            }
        }
        .task {
            do {
                title = try await fetchMovie(id: 42)
            } catch {
                errorMessage = "Could not load movie."
            }
        }
    }
}

The do/catch block works exactly the same as in synchronous code. The only difference is the await keyword telling Swift where the pause happens.

Common Mistakes

Forgetting await

Every call to an async function must use await. The compiler will catch this for you, but it’s the most common error when starting out.

// ❌ Don't do this — won't compile
let name = fetchToyName()
// ✅ Do this instead
let name = await fetchToyName()

Calling Async Functions Outside an Async Context

You can’t use await in a regular (synchronous) function. You need to be inside an async function or a Task block.

// ❌ Don't do this — not an async context
func loadData() {
    let name = await fetchToyName()
}
// ✅ Do this instead — wrap in a Task
func loadData() {
    Task {
        let name = await fetchToyName()
        print(name)
    }
}

Blocking the Main Thread with Heavy Work

async/await doesn’t automatically move work to a background thread. If you do CPU-heavy work in an async function, it can still block the UI. Use Task.detached or actors for truly heavy computation.

// ❌ Still runs on the main actor by default
func processAllMovies() async {
    // Heavy computation here still blocks UI
}
// ✅ Explicitly move to a background context
func processAllMovies() async {
    await Task.detached {
        // Heavy work runs off the main thread
    }.value
}

What’s Next?

  • async marks a function that can pause while waiting for slow work
  • await marks the point where your code pauses and resumes later
  • The .task modifier is the best way to call async code from SwiftUI views
  • Task { } creates an async context from synchronous code like button actions
  • Error handling in async code uses the same do/try/catch pattern — just add await

Ready to run multiple async operations at the same time? Head over to Tasks and Task Groups to learn about structured concurrency and parallel execution.