async / await
Swift Concurrency replaces nested completion handlers with async/await and structured task trees. Task, actor, and async let compose cleanly; TaskGroup handles parallelism; the compiler enforces data-race safety end to end.
async, Task, actor, TaskGroup, cancellation
EXAMPLE
// 1) Basic — async function + await
import Foundation
func fetchUser(id: Int) async throws -> User {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api.example.com/users/\(id)")!)
return try JSONDecoder().decode(User.self, from: data)
}
// Call from another async function
func loadProfile() async throws {
let user = try await fetchUser(id: 42)
print(user.name)
}
// 2) Top-level — wrap in Task
Task {
do {
try await loadProfile()
} catch {
print("failed: \(error)")
}
}
// 3) Sequential vs parallel
func sequential() async throws -> (User, Posts) {
let user = try await fetchUser(id: 1) // wait
let posts = try await fetchPosts(for: 1) // then wait
return (user, posts)
}
func parallel() async throws -> (User, Posts) {
async let user = fetchUser(id: 1) // start NOW
async let posts = fetchPosts(for: 1) // start NOW (in parallel)
return try await (user, posts) // wait for both
}
// async let starts the work immediately; await collects results.
// 4) TaskGroup — parallelise N dynamic items
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 results: [User] = []
for try await user in group {
results.append(user)
}
return results
}
}
// 5) Bounded concurrency — fixed concurrent inflight
func fetchAllBounded(ids: [Int], maxInFlight: Int = 4) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
var idx = 0
for _ in 0..<min(maxInFlight, ids.count) {
let id = ids[idx]; idx += 1
group.addTask { try await fetchUser(id: id) }
}
var results: [User] = []
for try await user in group {
results.append(user)
if idx < ids.count {
let id = ids[idx]; idx += 1
group.addTask { try await fetchUser(id: id) }
}
}
return results
}
}
// 6) Cancellation
let task = Task {
try await heavyWork()
}
task.cancel() // cooperative; the work must check
func heavyWork() async throws {
for i in 0..<1_000_000 {
try Task.checkCancellation() // throws CancellationError when cancelled
// ... work ...
}
}
// All async APIs in the stdlib check cancellation. URLSession data tasks abort on Task.cancel().
// 7) Sleep + delay
try await Task.sleep(for: .seconds(1)) // Swift 5.7+
try await Task.sleep(nanoseconds: 500_000_000)
// 8) Actors — opt-in serial-access classes
actor Counter {
private var value = 0
func increment() { value += 1 }
func current() -> Int { value }
}
let counter = Counter()
await counter.increment() // cross-actor calls need await
let v = await counter.current()
// Actors guarantee one thread at a time accesses their state — no data races by construction.
// 9) Main actor — UI work
@MainActor
class ViewModel: ObservableObject {
@Published var users: [User] = []
func load() async {
do {
users = try await fetchAll(ids: [1, 2, 3, 4])
} catch {
// handle
}
}
}
// @MainActor methods always run on the main thread; useful for SwiftUI publishers.
// Move work off the main actor:
func backgroundWork() async {
let data = await Task.detached(priority: .background) {
return heavyCompute() // runs off main actor
}.value
}
// 10) AsyncSequence — async iteration
for try await line in url.lines { // URL.lines is AsyncSequence<String>
print(line)
}
for try await event in client.subscribe() {
handle(event)
}
// Build your own:
struct Counter: AsyncSequence, AsyncIteratorProtocol {
typealias Element = Int
var current = 0
let max = 10
mutating func next() async throws -> Int? {
guard current < max else { return nil }
try await Task.sleep(for: .milliseconds(100))
defer { current += 1 }
return current
}
func makeAsyncIterator() -> Counter { self }
}
for try await n in Counter() { print(n) }
// 11) Continuations — bridge legacy callbacks to async
func fetchLegacy() async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
oldAPI(callback: { data, error in
if let error { continuation.resume(throwing: error) }
else { continuation.resume(returning: data!) }
})
}
}
// Use checked variants during dev; switch to unchecked after verifying single resume.
// 12) Sendable + data-race safety
// Types crossing actor boundaries must be Sendable:
// • Value types (struct/enum) with Sendable fields are auto-Sendable
// • Reference types must be 'final class' + immutable, or actor, or use @unchecked Sendable carefully
// • Swift 6 enables strict checking by default; Swift 5.10 has incremental flags
@MainActor
struct UserCard: View {
let user: User // User must be Sendable
var body: some View { Text(user.name) }
}
// 13) Errors
func parse() async throws -> Config {
do {
let data = try await loadData()
return try JSONDecoder().decode(Config.self, from: data)
} catch let e as DecodingError {
throw ConfigError.decode(e)
}
}
// 14) Patterns
// • Convert Combine pipelines to AsyncSequence with .values
// • Use @MainActor on view models + presenters
// • Detach with Task.detached when you need to escape the current actor's context
// • Cancel tasks in onDisappear or deinit
// • TimeoutTask via a TaskGroup race (race a sleep against the real work)
func withTimeout<T>(seconds: Double, _ work: @escaping @Sendable () async throws -> T) async throws -> T {
try await withThrowingTaskGroup(of: T.self) { group in
group.addTask { try await work() }
group.addTask {
try await Task.sleep(for: .seconds(seconds))
throw URLError(.timedOut)
}
defer { group.cancelAll() }
if let v = try await group.next() { return v }
throw URLError(.timedOut)
}
}
// 15) Common bugs
// • Forgot await — function suspended but not awaited → 'expression is async but not awaited'
// • Calling main-actor method from background → compile warning / error
// • Holding a Mutex across await — deadlock; use actor or @MainActor
// • Capturing self in a Task — strong reference; use [weak self]
// • TaskGroup not awaited — group cancelled when scope ends; collect results first
// • Not checking cancellation in long loops → tasks run after Task.cancel()
// • Detached tasks lose actor context — explicit @MainActor or actor parameter
// • Mixing async/await with completion handlers in the same flow — pick one
Why it matters
Swift Concurrency replaces nested callbacks with linear await. Run independent calls in parallel with async let, fan out N items with TaskGroup, gate UI updates with @MainActor, and let actors serialise mutable state for free. Cooperative cancellation works only if your code calls try Task.checkCancellation() in long loops.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
func fetchUser() async throws -> String {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://example.com")!)
return String(data: data, encoding: .utf8) ?? ""
}
Task {
let s = try await fetchUser()
print(s.prefix(40))
}
Try it Yourself »
Exercise
Mark a function as asynchronous.
func fetch()
throws -> String { /* … */ }
Five letters.
Discussion
Loading…