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?
- Calling an Async Function
- Writing Your Own Async Functions
- Using Task in SwiftUI
- Handling Errors in Async Code
- Common Mistakes
- What’s Next?
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
awaitinside a context that is alreadyasync— like anasyncfunction or aTaskblock. 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:
- Creates a URL from the movie ID.
- Uses
URLSessionto fetch data from the network — this is the slow part, so weawaitit. - Converts the raw data into a
Stringand 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
.taskoverTask { }inonAppearwhen the async work is tied to the view’s lifecycle. The.taskmodifier 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?
asyncmarks a function that can pause while waiting for slow workawaitmarks the point where your code pauses and resumes later- The
.taskmodifier 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/catchpattern — just addawait
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.