Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Diagnose and fix Swift Concurrency issues: async/await, actor isolation, Sendable, and Swift 6 migration.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/tasks.md
1# Tasks23Use this when:45- You need to start async work from synchronous code.6- You are choosing between `Task`, `async let`, and task groups.7- You need cancellation, priorities, or structured vs unstructured guidance.89Skip this file if:1011- The problem is mainly actor isolation or sendability. Use `actors.md` or `sendable.md`.12- The work is stream-shaped. Use `async-sequences.md` or `async-algorithms.md`.1314Jump to:1516- What is a Task?17- Cancellation18- Task Groups19- Discarding Task Groups20- Advanced: Task Timeout Pattern21- SwiftUI Integration22- Structured vs Unstructured Tasks23- Task Priorities2425## What is a Task?2627Tasks bridge synchronous and asynchronous contexts. They start executing immediately upon creation—no `resume()` needed.2829```swift30func synchronousMethod() {31Task {32await someAsyncMethod()33}34}35```363738### Task entry isolation3940`Task { ... }` inherits the enclosing isolation domain. This is especially easy to miss in modules that use `defaultIsolation(MainActor.self)` because bare tasks then start on `@MainActor` by default.4142Choose task entry isolation using the synchronous prefix rule (everything before the first `await`):43- If the prefix needs main-actor work, keep inherited `@MainActor` entry.44- If the prefix does not need main actor, prefer `Task { @concurrent in ... }` and hop back only for UI mutation.4546```swift47// ❌ Prefix has no main-actor work; first await hops away48Task {49await someActor.refresh()50}5152// ✅ Prefix needs @MainActor; keep inherited main start53Task {54print("debug") // trivial non-main line rides along55self.isLoading = true // main-actor state before first await56await fetchData()57}58```5960For deeper guidance and expanded examples, see `threading.md#choosing-task-entry-isolation`.6162## Task References6364Storing a reference is optional but enables cancellation and result waiting:6566```swift67final class ImageLoader {68var loadTask: Task<UIImage, Error>?6970func load() {71loadTask = Task {72try await fetchImage()73}74}7576deinit {77loadTask?.cancel()78}79}80```8182Tasks run regardless of whether you keep a reference.8384> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.1: Introduction to tasks in Swift Concurrency](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)8586## Cancellation8788### Checking for cancellation8990Tasks must manually check for cancellation:9192```swift93// Throws CancellationError if canceled94try Task.checkCancellation()9596// Boolean check for custom handling97guard !Task.isCancelled else {98return fallbackValue99}100```101102### Where to check103104Add checks at natural breakpoints:105106```swift107let task = Task {108// Before expensive work109try Task.checkCancellation()110111let data = try await URLSession.shared.data(from: url)112113// After network, before processing114try Task.checkCancellation()115116return processData(data)117}118```119120### Child task cancellation121122Canceling a parent automatically notifies all children:123124```swift125let parent = Task {126async let child1 = work(1)127async let child2 = work(2)128let results = try await [child1, child2]129}130131parent.cancel() // Both children notified132```133134Children must still check `Task.isCancelled` to stop work.135136> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.2: Task cancellation](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)137138## Error Handling139140Task error types are inferred from the operation:141142```swift143// Can throw144let throwingTask: Task<String, Error> = Task {145throw URLError(.badURL)146}147148// Cannot throw149let nonThrowingTask: Task<String, Never> = Task {150"Success"151}152```153154### Awaiting results155156```swift157do {158let result = try await task.value159} catch {160// Handle error161}162```163164### Handling errors internally165166```swift167let safeTask: Task<String, Never> = Task {168do {169return try await riskyOperation()170} catch {171return "Fallback value"172}173}174```175176> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.3: Error handling in Tasks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)177178## SwiftUI Integration179180### The .task modifier181182Automatically manages task lifetime with view lifecycle:183184```swift185struct ContentView: View {186@State private var data: Data?187188var body: some View {189Text(data?.description ?? "Loading...")190.task {191data = try? await fetchData()192}193}194}195```196197Task cancels automatically when view disappears.198199### Reacting to value changes200201```swift202.task(id: searchQuery) {203await performSearch(searchQuery)204}205```206207When `searchQuery` changes:2081. Previous task cancels2092. New task starts with updated value210211> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.12: Running tasks in SwiftUI](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)212213### Priority configuration214215```swift216// High priority (default for SwiftUI)217.task(priority: .userInitiated) {218await fetchUserData()219}220221// Lower priority for background work222.task(priority: .low) {223await trackAnalytics()224}225```226227## Task Groups228229Dynamic parallel task execution with compile-time unknown task count.230231### Basic usage232233```swift234await withTaskGroup(of: UIImage.self) { group in235for url in photoURLs {236group.addTask {237await downloadPhoto(url: url)238}239}240}241```242243### Collecting results244245```swift246let images = await withTaskGroup(of: UIImage.self) { group in247for url in photoURLs {248group.addTask { await downloadPhoto(url: url) }249}250251return await group.reduce(into: []) { $0.append($1) }252}253```254255### Error handling256257```swift258let images = try await withThrowingTaskGroup(of: UIImage.self) { group in259for url in photoURLs {260group.addTask { try await downloadPhoto(url: url) }261}262263// Iterate to propagate errors264var results: [UIImage] = []265for try await image in group {266results.append(image)267}268return results269}270```271272**Critical**: Errors in child tasks don't automatically fail the group. Use iteration (`for try await`, `next()`, `reduce()`) to propagate errors.273274> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.5: Task Groups](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)275276### Early termination on error277278```swift279try await withThrowingTaskGroup(of: Data.self) { group in280for id in ids {281group.addTask { try await fetch(id) }282}283284// First error cancels remaining tasks285while let data = try await group.next() {286process(data)287}288}289```290291### Cancellation292293```swift294await withTaskGroup(of: Result.self) { group in295for item in items {296group.addTask { await process(item) }297}298299// Cancel all remaining tasks300group.cancelAll()301}302```303304Or prevent adding to canceled group:305306```swift307let didAdd = group.addTaskUnlessCancelled {308await work()309}310```311312## Discarding Task Groups313314For fire-and-forget operations where results don't matter:315316```swift317await withDiscardingTaskGroup { group in318group.addTask { await logEvent("user_login") }319group.addTask { await preloadCache() }320group.addTask { await syncAnalytics() }321}322```323324### Benefits325326- More memory efficient (doesn't store results)327- No `next()` calls needed328- Automatically waits for completion329- Ideal for side effects330331### Error handling332333```swift334try await withThrowingDiscardingTaskGroup { group in335group.addTask { try await uploadLog() }336group.addTask { try await syncSettings() }337}338// First error cancels group and throws339```340341### Real-world pattern: Multiple notifications342343```swift344extension NotificationCenter {345func notifications(named names: [Notification.Name]) -> AsyncStream<()> {346AsyncStream { continuation in347let task = Task {348await withDiscardingTaskGroup { group in349for name in names {350group.addTask {351for await _ in self.notifications(named: name) {352continuation.yield(())353}354}355}356}357continuation.finish()358}359360continuation.onTermination = { _ in task.cancel() }361}362}363}364365// Usage366for await _ in NotificationCenter.default.notifications(367named: [.userDidLogin, UIApplication.didBecomeActiveNotification]368) {369refreshData()370}371```372373> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.6: Discarding Task Groups](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)374375## Structured vs Unstructured Tasks376377### Structured (preferred)378379Bound to parent, inherit context, automatic cancellation:380381```swift382// async let383async let data1 = fetch(1)384async let data2 = fetch(2)385let results = await [data1, data2]386387// Task groups388await withTaskGroup(of: Data.self) { group in389group.addTask { await fetch(1) }390group.addTask { await fetch(2) }391}392```393394> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.7: The difference between structured and unstructured tasks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)395396### Unstructured (use sparingly)397398Independent lifecycle, manual cancellation:399400```swift401// Regular task (unstructured but inherits priority)402let task = Task {403await doWork()404}405406// Detached task (completely independent)407Task.detached(priority: .background) {408await cleanup()409}410```411412## Detached Tasks413414**Use as last resort.** They don't inherit:415- Priority416- Task-local values417- Cancellation state418419```swift420Task.detached(priority: .background) {421await DirectoryCleaner.cleanup()422}423```424425### When to use426427- Independent background work428- No connection to parent needed429- Acceptable to complete after parent cancels430- No `self` references needed431432**Prefer**: Task groups or `async let` for most parallel work.433434> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.4: Detached Tasks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)435436## Task Priorities437438### Available priorities439440```swift441.high // Immediate user feedback442.userInitiated // User-triggered work (same as .high)443.medium // Default for detached tasks444.utility // Longer-running, non-urgent445.low // Similar to .background446.background // Lowest priority447```448449### Setting priority450451```swift452Task(priority: .background) {453await prefetchData()454}455```456457### Priority inheritance458459Structured tasks inherit parent priority:460461```swift462Task(priority: .high) {463async let result = work() // Also .high464await result465}466```467468Detached tasks don't inherit:469470```swift471Task(priority: .high) {472Task.detached {473// Runs at .medium (default)474}475}476```477478### Priority escalation479480System automatically elevates priority to prevent priority inversion:481- Actor waiting on lower-priority task482- High-priority task awaiting `.value` of lower-priority task483484> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.8: Managing Task priorities](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)485486## Task.sleep() vs Task.yield()487488### Task.sleep()489490Suspends for fixed duration, non-blocking:491492```swift493try await Task.sleep(for: .seconds(5))494```495496**Use for:**497- Debouncing user input498- Polling intervals499- Rate limiting500- Artificial delays501502**Respects cancellation** (throws `CancellationError`)503504### Task.yield()505506Temporarily suspends to allow other tasks to run:507508```swift509await Task.yield()510```511512**Use for:**513- Testing async code514- Allowing cooperative scheduling515516**Note**: If current task is highest priority, may resume immediately.517518### Practical: Debounced search519520```swift521func search(_ query: String) async {522guard !query.isEmpty else {523searchResults = allResults524return525}526527do {528try await Task.sleep(for: .milliseconds(500))529searchResults = allResults.filter { $0.contains(query) }530} catch {531// Canceled (user kept typing)532}533}534535// In SwiftUI536.task(id: searchQuery) {537await searcher.search(searchQuery)538}539```540541> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.10: Task.yield() vs. Task.sleep()](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)542543## async let vs TaskGroup544545| Feature | async let | TaskGroup |546|---------|-----------|-----------|547| Task count | Fixed at compile-time | Dynamic at runtime |548| Syntax | Lightweight | More verbose |549| Cancellation | Automatic on scope exit | Manual via `cancelAll()` |550| Use when | 2-5 known parallel tasks | Loop-based parallel work |551552```swift553// async let: Known task count554async let user = fetchUser()555async let settings = fetchSettings()556let profile = Profile(user: await user, settings: await settings)557558// TaskGroup: Dynamic task count559await withTaskGroup(of: Image.self) { group in560for url in urls {561group.addTask { await download(url) }562}563}564```565566## Advanced: Task Timeout Pattern567568Create timeout wrapper using task groups:569570```swift571func withTimeout<T>(572_ duration: Duration,573operation: @Sendable @escaping () async throws -> T574) async throws -> T {575try await withThrowingTaskGroup(of: T.self) { group in576group.addTask { try await operation() }577578group.addTask {579try await Task.sleep(for: duration)580throw TimeoutError()581}582583guard let result = try await group.next() else {584throw TimeoutError()585}586587group.cancelAll()588return result589}590}591592// Usage593let data = try await withTimeout(.seconds(5)) {594try await slowNetworkRequest()595}596```597598**`cancelAll()` is critical** — without it, the losing task keeps running until scope exit.599600`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.601602> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.14: Creating a Task timeout handler using a Task Group (advanced)](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)603604## Common Patterns605606### Sequential with early exit607608```swift609let user = try await fetchUser()610guard user.isActive else { return }611612let posts = try await fetchPosts(userId: user.id)613```614615### Parallel independent work616617```swift618async let user = fetchUser()619async let settings = fetchSettings()620async let notifications = fetchNotifications()621622let data = try await (user, settings, notifications)623```624625### Mixed: Sequential then parallel626627```swift628let user = try await fetchUser()629630async let posts = fetchPosts(userId: user.id)631async let followers = fetchFollowers(userId: user.id)632633let profile = Profile(634user: user,635posts: try await posts,636followers: try await followers637)638```639640## Common Mistakes Agents Make641642- Replacing structured child work with many unrelated top-level tasks.643- Using `Task.detached` just to "make it background."644- Ignoring cancellation in long-running operations.645- Keeping a stored task forever without a clear owner or cleanup path.646- Picking entry isolation from the enclosing context rather than the task's synchronous prefix — `Task { await someActor.x() }` from a `@MainActor` context should be `Task { @concurrent in ... }`; a `Task` whose prefix mutates `@MainActor` state should stay on inherited `@MainActor` even if it also has a `print`.647- Priorities are hints, not guarantees. The system automatically elevates priority to prevent inversion (e.g., a high-priority task awaiting `.value` of a lower-priority task). Do not rely on priority for correctness.648649## Best Practices6506511. **Check cancellation regularly** in long-running tasks6522. **Use structured concurrency** (avoid detached tasks)6533. **Leverage SwiftUI's `.task` modifier** for view-bound work6544. **Choose the right tool**: `async let` for fixed, TaskGroup for dynamic6555. **Handle errors explicitly** in throwing task groups6566. **Set priority only when needed** (inherit by default)6577. **Don't mutate task groups** from outside their creation context658659## Further Learning660661For hands-on examples, advanced patterns, and migration strategies, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com).662663