.agents/skills/swift-concurrency/references/tasks.md
Use this when:
Task, async let, and task groups.Skip this file if:
actors.md or sendable.md.async-sequences.md or async-algorithms.md.Jump to:
Tasks bridge synchronous and asynchronous contexts. They start executing immediately upon creation—no resume() needed.
func synchronousMethod() {
Task {
await someAsyncMethod()
}
}
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.
Course Deep Dive: This topic is covered in detail in Lesson 3.1: Introduction to tasks in Swift Concurrency
Tasks must manually check for cancellation:
// Throws CancellationError if canceled
try Task.checkCancellation()
// Boolean check for custom handling
guard !Task.isCancelled else {
return fallbackValue
}
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)
}
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 notified
Children must still check Task.isCancelled to stop work.
Course Deep Dive: This topic is covered in detail in Lesson 3.2: Task cancellation
Task error types are inferred from the operation:
// Can throw
let throwingTask: Task<String, Error> = Task {
throw URLError(.badURL)
}
// Cannot throw
let nonThrowingTask: Task<String, Never> = Task {
"Success"
}
do {
let result = try await task.value
} catch {
// Handle error
}
let safeTask: Task<String, Never> = Task {
do {
return try await riskyOperation()
} catch {
return "Fallback value"
}
}
Course Deep Dive: This topic is covered in detail in Lesson 3.3: Error handling in Tasks
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.
.task(id: searchQuery) {
await performSearch(searchQuery)
}
When searchQuery changes:
Course Deep Dive: This topic is covered in detail in Lesson 3.12: Running tasks in SwiftUI
// High priority (default for SwiftUI)
.task(priority: .userInitiated) {
await fetchUserData()
}
// Lower priority for background work
.task(priority: .low) {
await trackAnalytics()
}
Dynamic parallel task execution with compile-time unknown task count.
await withTaskGroup(of: UIImage.self) { group in
for url in photoURLs {
group.addTask {
await downloadPhoto(url: url)
}
}
}
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) }
}
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.
Course Deep Dive: This topic is covered in detail in Lesson 3.5: Task Groups
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)
}
}
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()
}
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() }
}
next() calls neededtry await withThrowingDiscardingTaskGroup { group in
group.addTask { try await uploadLog() }
group.addTask { try await syncSettings() }
}
// First error cancels group and throws
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() }
}
}
}
// Usage
for await _ in NotificationCenter.default.notifications(
named: [.userDidLogin, UIApplication.didBecomeActiveNotification]
) {
refreshData()
}
Course Deep Dive: This topic is covered in detail in Lesson 3.6: Discarding Task Groups
Bound to parent, inherit context, automatic cancellation:
// async let
async let data1 = fetch(1)
async let data2 = fetch(2)
let results = await [data1, data2]
// Task groups
await withTaskGroup(of: Data.self) { group in
group.addTask { await fetch(1) }
group.addTask { await fetch(2) }
}
Course Deep Dive: This topic is covered in detail in Lesson 3.7: The difference between structured and unstructured tasks
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()
}
Use as last resort. They don't inherit:
Task.detached(priority: .background) {
await DirectoryCleaner.cleanup()
}
self references neededPrefer: Task groups or async let for most parallel work.
Course Deep Dive: This topic is covered in detail in Lesson 3.4: Detached Tasks
.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 priority
Task(priority: .background) {
await prefetchData()
}
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)
}
}
System automatically elevates priority to prevent priority inversion:
.value of lower-priority taskCourse Deep Dive: This topic is covered in detail in Lesson 3.8: Managing Task priorities
Suspends for fixed duration, non-blocking:
try await Task.sleep(for: .seconds(5))
Use for:
Respects cancellation (throws CancellationError)
Temporarily suspends to allow other tasks to run:
await Task.yield()
Use for:
Note: If current task is highest priority, may resume immediately.
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)
}
Course Deep Dive: This topic is covered in detail in Lesson 3.10: Task.yield() vs. Task.sleep()
| 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 count
async let user = fetchUser()
async let settings = fetchSettings()
let profile = Profile(user: await user, settings: await settings)
// TaskGroup: Dynamic task count
await withTaskGroup(of: Image.self) { group in
for url in urls {
group.addTask { await download(url) }
}
}
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
}
}
// Usage
let 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.
Course Deep Dive: This topic is covered in detail in Lesson 3.14: Creating a Task timeout handler using a Task Group (advanced)
let user = try await fetchUser()
guard user.isActive else { return }
let posts = try await fetchPosts(userId: user.id)
async let user = fetchUser()
async let settings = fetchSettings()
async let notifications = fetchNotifications()
let data = try await (user, settings, notifications)
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
)
Task.detached just to "make it background.".value of a lower-priority task). Do not rely on priority for correctness..task modifier for view-bound workasync let for fixed, TaskGroup for dynamicFor hands-on examples, advanced patterns, and migration strategies, see Swift Concurrency Course.