Introduction
Swift's TaskGroup provides structured concurrency for running multiple tasks in parallel. However, error handling with task groups is nuanced: withThrowingTaskGroup cancels all remaining children when the first child throws, while withTaskGroup requires manual error collection. Understanding this behavior is critical for building resilient concurrent code.
Symptoms
- One failing task cancels all other in-flight tasks unexpectedly
- Partial results from successful tasks are lost when one task fails
for awaitloop over task group exits early on first error- Child task errors do not propagate to the parent as expected
- Cancellation of parent task does not properly cancel children
Example problematic code: ```swift // All tasks cancelled when the first one throws let results = try await withThrowingTaskGroup(of: Item.self) { group in for url in urls { group.addTask { try await fetchItem(from: url) // If this throws, all other tasks are cancelled } }
var results: [Item] = [] for try await item in group { results.append(item) } return results } ```
Common Causes
- Using
withThrowingTaskGroupwhen partial results are acceptable - Not handling individual task errors before they reach the group
- Assuming
withThrowingTaskGroupcollects all errors - Child tasks throw different error types than expected
- Group exits early when
breakis called infor awaitloop
Step-by-Step Fix
- 1.Collect partial results with error tracking:
- 2.```swift
- 3.func fetchAllItems(from urls: [URL]) async -> (items: [Item], errors: [Error]) {
- 4.await withTaskGroup(of: Result<Item, Error>.self) { group in
- 5.var items: [Item] = []
- 6.var errors: [Error] = []
for url in urls { group.addTask { do { let item = try await fetchItem(from: url) return .success(item) } catch { return .failure(error) } } }
for await result in group { switch result { case .success(let item): items.append(item) case .failure(let error): errors.append(error) } }
return (items, errors) } } ```
- 1.Fail fast with withThrowingTaskGroup when all results are required:
- 2.```swift
- 3.func fetchAllRequired(from urls: [URL]) async throws -> [Item] {
- 4.try await withThrowingTaskGroup(of: Item.self) { group in
- 5.var results: [Item] = []
for url in urls { group.addTask { try await fetchItem(from: url) } }
for try await item in group { results.append(item) }
return results } // If any task throws, the entire group is cancelled and the error propagates } ```
- 1.Handle specific errors without cancelling the group:
- 2.```swift
- 3.func processWithRecovery(from urls: [URL]) async -> [Item] {
- 4.await withTaskGroup(of: Item?.self) { group in
- 5.var results: [Item] = []
for url in urls { group.addTask { do { return try await fetchItem(from: url) } catch FetchError.notFound { print("Not found: \(url) - skipping") return nil } catch FetchError.rateLimited { // Retry with backoff try? await Task.sleep(for: .seconds(5)) return try? await fetchItem(from: url) } catch { print("Unexpected error for \(url): \(error)") return nil } } }
for await item in group { if let item = item { results.append(item) } }
return results } } ```
- 1.Properly cancel child tasks on parent cancellation:
- 2.```swift
- 3.func fetchWithTimeout(from urls: [URL], timeout: Duration) async -> [Item] {
- 4.await withTaskGroup(of: Item.self) { group in
- 5.for url in urls {
- 6.group.addTask {
- 7.// Check for cancellation before starting work
- 8.try? Task.checkCancellation()
let item = try? await fetchItem(from: url)
// Check for cancellation after work try? Task.checkCancellation()
return item! } }
var results: [Item] = [] for await item in group { if Task.isCancelled { group.cancelAll() // Cancel remaining children break } results.append(item) }
return results } } ```
Prevention
- Use
withTaskGroup(not throwing) when partial results are acceptable - Wrap individual task errors in
Resulttypes to prevent group cancellation - Use
withThrowingTaskGroupwhen all tasks must succeed - Always check
Task.isCancelledin long-running child tasks - Document whether a function is fail-fast or partial-results
- Add structured concurrency tests that verify error propagation behavior