Tasks
Use this when:
- You need to start async work from synchronous code.
- You are choosing between
Task,async let, and task groups. - You need cancellation, priorities, or structured vs unstructured guidance.
Skip this file if:
- The problem is mainly actor isolation or sendability. Use
actors.mdorsendable.md. - The work is stream-shaped. Use
async-sequences.mdorasync-algorithms.md.
Jump to:
- What is a Task?
- Cancellation
- Task Groups
- Discarding Task Groups
- Advanced: Task Timeout Pattern
- SwiftUI Integration
- Structured vs Unstructured Tasks
- Task Priorities
What is a Task?
Section titled “What is a Task?”Tasks bridge synchronous and asynchronous contexts. They start executing immediately upon creation—no resume() needed.
func synchronousMethod() { Task { await someAsyncMethod() }}Task References
Section titled “Task References”Storing a reference is optional but enables cancellation and result waiting:
final class ImageLoader { var loadTask: Task<UIImage, Error>?
func load() { loadTask = Task { try await fetchImage() } }
deinit { loadTask?.cancel() }}Tasks run regardless of whether you keep a reference.
Cancellation
Section titled “Cancellation”Checking for cancellation
Section titled “Checking for cancellation”Tasks must manually check for cancellation:
// Throws CancellationError if canceledtry Task.checkCancellation()
// Boolean check for custom handlingguard !Task.isCancelled else { return fallbackValue}Where to check
Section titled “Where to check”Add checks at natural breakpoints:
let task = Task { // Before expensive work try Task.checkCancellation()
let data = try await URLSession.shared.data(from: url)
// After network, before processing try Task.checkCancellation()
return processData(data)}Child task cancellation
Section titled “Child task cancellation”Canceling a parent automatically notifies all children:
let parent = Task { async let child1 = work(1) async let child2 = work(2) let results = try await [child1, child2]}
parent.cancel() // Both children notifiedChildren must still check Task.isCancelled to stop work.
Error Handling
Section titled “Error Handling”Task error types are inferred from the operation:
// Can throwlet throwingTask: Task<String, Error> = Task { throw URLError(.badURL)}
// Cannot throwlet nonThrowingTask: Task<String, Never> = Task { "Success"}Awaiting results
Section titled “Awaiting results”do { let result = try await task.value} catch { // Handle error}Handling errors internally
Section titled “Handling errors internally”let safeTask: Task<String, Never> = Task { do { return try await riskyOperation() } catch { return "Fallback value" }}SwiftUI Integration
Section titled “SwiftUI Integration”The .task modifier
Section titled “The .task modifier”Automatically manages task lifetime with view lifecycle:
struct ContentView: View { @State private var data: Data?
var body: some View { Text(data?.description ?? "Loading...") .task { data = try? await fetchData() } }}Task cancels automatically when view disappears.
Reacting to value changes
Section titled “Reacting to value changes”.task(id: searchQuery) { await performSearch(searchQuery)}When searchQuery changes:
- Previous task cancels
- New task starts with updated value
Priority configuration
Section titled “Priority configuration”// High priority (default for SwiftUI).task(priority: .userInitiated) { await fetchUserData()}
// Lower priority for background work.task(priority: .low) { await trackAnalytics()}Task Groups
Section titled “Task Groups”Dynamic parallel task execution with compile-time unknown task count.
Basic usage
Section titled “Basic usage”await withTaskGroup(of: UIImage.self) { group in for url in photoURLs { group.addTask { await downloadPhoto(url: url) } }}Collecting results
Section titled “Collecting results”let images = await withTaskGroup(of: UIImage.self) { group in for url in photoURLs { group.addTask { await downloadPhoto(url: url) } }
return await group.reduce(into: []) { $0.append($1) }}Error handling
Section titled “Error handling”let images = try await withThrowingTaskGroup(of: UIImage.self) { group in for url in photoURLs { group.addTask { try await downloadPhoto(url: url) } }
// Iterate to propagate errors var results: [UIImage] = [] for try await image in group { results.append(image) } return results}Critical: Errors in child tasks don’t automatically fail the group. Use iteration (for try await, next(), reduce()) to propagate errors.
Early termination on error
Section titled “Early termination on error”try await withThrowingTaskGroup(of: Data.self) { group in for id in ids { group.addTask { try await fetch(id) } }
// First error cancels remaining tasks while let data = try await group.next() { process(data) }}Cancellation
Section titled “Cancellation”await withTaskGroup(of: Result.self) { group in for item in items { group.addTask { await process(item) } }
// Cancel all remaining tasks group.cancelAll()}Or prevent adding to canceled group:
let didAdd = group.addTaskUnlessCancelled { await work()}Discarding Task Groups
Section titled “Discarding Task Groups”For fire-and-forget operations where results don’t matter:
await withDiscardingTaskGroup { group in group.addTask { await logEvent("user_login") } group.addTask { await preloadCache() } group.addTask { await syncAnalytics() }}Benefits
Section titled “Benefits”- More memory efficient (doesn’t store results)
- No
next()calls needed - Automatically waits for completion
- Ideal for side effects
Error handling
Section titled “Error handling”try await withThrowingDiscardingTaskGroup { group in group.addTask { try await uploadLog() } group.addTask { try await syncSettings() }}// First error cancels group and throwsReal-world pattern: Multiple notifications
Section titled “Real-world pattern: Multiple notifications”extension NotificationCenter { func notifications(named names: [Notification.Name]) -> AsyncStream<()> { AsyncStream { continuation in let task = Task { await withDiscardingTaskGroup { group in for name in names { group.addTask { for await _ in self.notifications(named: name) { continuation.yield(()) } } } } continuation.finish() }
continuation.onTermination = { _ in task.cancel() } } }}
// Usagefor await _ in NotificationCenter.default.notifications( named: [.userDidLogin, UIApplication.didBecomeActiveNotification]) { refreshData()}Structured vs Unstructured Tasks
Section titled “Structured vs Unstructured Tasks”Structured (preferred)
Section titled “Structured (preferred)”Bound to parent, inherit context, automatic cancellation:
// async letasync let data1 = fetch(1)async let data2 = fetch(2)let results = await [data1, data2]
// Task groupsawait withTaskGroup(of: Data.self) { group in group.addTask { await fetch(1) } group.addTask { await fetch(2) }}Unstructured (use sparingly)
Section titled “Unstructured (use sparingly)”Independent lifecycle, manual cancellation:
// Regular task (unstructured but inherits priority)let task = Task { await doWork()}
// Detached task (completely independent)Task.detached(priority: .background) { await cleanup()}Detached Tasks
Section titled “Detached Tasks”Use as last resort. They don’t inherit:
- Priority
- Task-local values
- Cancellation state
Task.detached(priority: .background) { await DirectoryCleaner.cleanup()}When to use
Section titled “When to use”- Independent background work
- No connection to parent needed
- Acceptable to complete after parent cancels
- No
selfreferences needed
Prefer: Task groups or async let for most parallel work.
Task Priorities
Section titled “Task Priorities”Available priorities
Section titled “Available priorities”.high // Immediate user feedback.userInitiated // User-triggered work (same as .high).medium // Default for detached tasks.utility // Longer-running, non-urgent.low // Similar to .background.background // Lowest prioritySetting priority
Section titled “Setting priority”Task(priority: .background) { await prefetchData()}Priority inheritance
Section titled “Priority inheritance”Structured tasks inherit parent priority:
Task(priority: .high) { async let result = work() // Also .high await result}Detached tasks don’t inherit:
Task(priority: .high) { Task.detached { // Runs at .medium (default) }}Priority escalation
Section titled “Priority escalation”System automatically elevates priority to prevent priority inversion:
- Actor waiting on lower-priority task
- High-priority task awaiting
.valueof lower-priority task
Task.sleep() vs Task.yield()
Section titled “Task.sleep() vs Task.yield()”Task.sleep()
Section titled “Task.sleep()”Suspends for fixed duration, non-blocking:
try await Task.sleep(for: .seconds(5))Use for:
- Debouncing user input
- Polling intervals
- Rate limiting
- Artificial delays
Respects cancellation (throws CancellationError)
Task.yield()
Section titled “Task.yield()”Temporarily suspends to allow other tasks to run:
await Task.yield()Use for:
- Testing async code
- Allowing cooperative scheduling
Note: If current task is highest priority, may resume immediately.
Practical: Debounced search
Section titled “Practical: Debounced search”func search(_ query: String) async { guard !query.isEmpty else { searchResults = allResults return }
do { try await Task.sleep(for: .milliseconds(500)) searchResults = allResults.filter { $0.contains(query) } } catch { // Canceled (user kept typing) }}
// In SwiftUI.task(id: searchQuery) { await searcher.search(searchQuery)}async let vs TaskGroup
Section titled “async let vs TaskGroup”| Feature | async let | TaskGroup |
|---|---|---|
| Task count | Fixed at compile-time | Dynamic at runtime |
| Syntax | Lightweight | More verbose |
| Cancellation | Automatic on scope exit | Manual via cancelAll() |
| Use when | 2-5 known parallel tasks | Loop-based parallel work |
// async let: Known task countasync let user = fetchUser()async let settings = fetchSettings()let profile = Profile(user: await user, settings: await settings)
// TaskGroup: Dynamic task countawait withTaskGroup(of: Image.self) { group in for url in urls { group.addTask { await download(url) } }}Advanced: Task Timeout Pattern
Section titled “Advanced: Task Timeout Pattern”Create timeout wrapper using task groups:
func withTimeout<T>( _ duration: Duration, operation: @Sendable @escaping () async throws -> T) async throws -> T { try await withThrowingTaskGroup(of: T.self) { group in group.addTask { try await operation() }
group.addTask { try await Task.sleep(for: duration) throw TimeoutError() }
guard let result = try await group.next() else { throw TimeoutError() }
group.cancelAll() return result }}
// Usagelet data = try await withTimeout(.seconds(5)) { try await slowNetworkRequest()}cancelAll() is critical — without it, the losing task keeps running until scope exit.
Task.sleep throws CancellationError when the task is cancelled, making it a useful cancellation checkpoint in polling loops. Task.yield() only gives other tasks a chance to run and does not check cancellation — if the current task has the highest priority, it may resume immediately.
Common Patterns
Section titled “Common Patterns”Sequential with early exit
Section titled “Sequential with early exit”let user = try await fetchUser()guard user.isActive else { return }
let posts = try await fetchPosts(userId: user.id)Parallel independent work
Section titled “Parallel independent work”async let user = fetchUser()async let settings = fetchSettings()async let notifications = fetchNotifications()
let data = try await (user, settings, notifications)Mixed: Sequential then parallel
Section titled “Mixed: Sequential then parallel”let user = try await fetchUser()
async let posts = fetchPosts(userId: user.id)async let followers = fetchFollowers(userId: user.id)
let profile = Profile( user: user, posts: try await posts, followers: try await followers)Common Mistakes Agents Make
Section titled “Common Mistakes Agents Make”- Replacing structured child work with many unrelated top-level tasks.
- Using
Task.detachedjust to “make it background.” - Ignoring cancellation in long-running operations.
- Keeping a stored task forever without a clear owner or cleanup path.
- Priorities are hints, not guarantees. The system automatically elevates priority to prevent inversion (e.g., a high-priority task awaiting
.valueof a lower-priority task). Do not rely on priority for correctness.
Best Practices
Section titled “Best Practices”- Check cancellation regularly in long-running tasks
- Use structured concurrency (avoid detached tasks)
- Leverage SwiftUI’s
.taskmodifier for view-bound work - Choose the right tool:
async letfor fixed, TaskGroup for dynamic - Handle errors explicitly in throwing task groups
- Set priority only when needed (inherit by default)
- Don’t mutate task groups from outside their creation context