Reading — step 1 of 6
Learn
Swift 5.5 added native async/await and structured concurrency. The model is similar to JavaScript and C#, but Swift's takes type-safety and cancellation seriously.
Basic async/await
func fetchUser(id: Int) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
// Calling — must be in an async context:
let user = try await fetchUser(id: 42)
async marks a function as suspending — it can pause without blocking a thread. await is required at every call site to make suspension visible.
Tasks — top-level async work
Most code starts an async chain via a Task:
let task = Task {
let user = try await fetchUser(id: 42)
print(user.name)
}
// Cancel from outside:
task.cancel()
Parallel execution with async let
func fetchProfile(id: Int) async throws -> (User, [Post]) {
async let user = fetchUser(id: id)
async let posts = fetchPosts(userId: id)
return try await (user, posts) // both run in parallel
}
async let kicks off the work immediately. The await later collects the result. Total time = max(individual times), not sum.
TaskGroup — N parallel tasks
func fetchAll(ids: [Int]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask {
try await fetchUser(id: id)
}
}
var users: [User] = []
for try await user in group {
users.append(user)
}
return users
}
}
Dynamic number of parallel tasks. Cancellation propagates: if you throw, all sibling tasks are cancelled.
Cancellation
Cancellation in Swift is cooperative — your code must check:
func longComputation() async throws -> Int {
var result = 0
for i in 0..<1_000_000 {
try Task.checkCancellation() // throws CancellationError if cancelled
result += i
}
return result
}
Most standard library async APIs check cancellation automatically. Long-running custom loops should call Task.checkCancellation periodically.
Actors — thread-safe shared state
Swift 5.5 also added actors for protecting shared state:
actor BankAccount {
private var balance: Int = 0
func deposit(_ amount: Int) {
balance += amount
}
func getBalance() -> Int {
return balance
}
}
let account = BankAccount()
await account.deposit(100) // await — entering the actor
let b = await account.getBalance() // serialized access
Actors serialize access to their state — only one task can be inside an actor's methods at a time. Race conditions on actor state become impossible.
Common mistakes
- Calling async from sync without a Task — compile error. Wrap in
Task { ... }. - Forgetting
tryfor throwing async calls —try awaitis the common combo. - Awaiting in a loop when independent items could run in parallel — use
async letor TaskGroup. - Nested
Task { }in actor methods unintentionally — the inner Task is detached from the actor's isolation. Useawaitinstead when you want serialized access.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…