TaskGroup: Mastering Dynamic Parallelism in Swift

Published on · Updated · 20 min

Wlad
Wlad
Founder & Swift Tech Lead

TaskGroup is the API of choice for executing an arbitrary number of tasks in parallel and collecting their results. Unlike async let which requires knowing the number of tasks at compile time, TaskGroup allows creating tasks dynamically at runtime. This guide covers everything from fundamentals to advanced patterns with DiscardingTaskGroup.

Why TaskGroup?

async let is perfect when you know the exact number of tasks:

But what if you need to download 1000 images? Writing 1000 async let statements isn't feasible. That's where TaskGroup comes in:

TaskGroup Variants

Swift provides four types of task groups:

TypeThrowingResults

withTaskGroup

No

Collected

withThrowingTaskGroup

Yes

Collected

withDiscardingTaskGroup

No

Discarded

withThrowingDiscardingTaskGroup

Yes

Discarded

Basic Syntax

Creating and Adding Tasks

Basic withTaskGroup

Task Priority

Each task can have its own priority:

addTaskUnlessCancelled

To avoid adding tasks if the group is already cancelled:

Collecting Results

Iteration with for await

The most common method for collecting results:

Important: The order of results is NOT guaranteed. Tasks complete in any order depending on their duration.

Preserving Order with Tuples

To maintain the original order, include the index:

Using reduce

To aggregate results directly:

Using next()

For fine-grained control over result consumption:

waitForAll()

To wait for all tasks without collecting results:

Error Handling with ThrowingTaskGroup

Default Behavior: Fail-Fast

With withThrowingTaskGroup, the first error is propagated and the group is cancelled:

Collect-All: Continue Despite Errors

To collect all results even if some tasks fail:

Dashboard Pattern: Independent Sections

Cancellation

cancelAll()

To cancel all remaining tasks:

isCancelled and checkCancellation

Tasks must check for cancellation cooperatively:

Automatic Cancellation on Exit

The group always waits for all tasks before returning, even after cancelAll():

DiscardingTaskGroup (Swift 5.9+)

The Problem with Classic TaskGroup

With a standard TaskGroup, if you don't call next() or iterate over the group, completed tasks remain in memory:

Solution: DiscardingTaskGroup

DiscardingTaskGroup automatically cleans up completed tasks:

Ideal Use Cases

DiscardingTaskGroup is perfect for:

    • HTTP/WebSocket servers: infinite connection loops
    • File watchers: continuous file monitoring
    • Event listeners: continuous event processing
    • Background workers: tasks with no results to collect

Differences from TaskGroup

AspectTaskGroupDiscardingTaskGroup

Results

Collected via next()/for await

Immediately discarded

Memory

Tasks kept until consumed

Released when completed

AsyncSequence

Yes

No

next()

Available

Not available

Use case

Collect results

Fire-and-forget, servers

Advanced Patterns

Limiting Concurrency

TaskGroup doesn't limit the number of concurrent tasks by default. Here's how to do it:

Global Timeout

Race: First Come, First Served

Retry with Parallelism

Heterogeneous Types with Enum

For tasks that return different types:

Best Practices

1. Always Consume Results or Use DiscardingTaskGroup

2. Handle Cancellation in Tasks

3. Avoid Strong Captures of self

4. Use the Right Group Type

SituationRecommended Type

Collect results

withTaskGroup

Tasks that can fail

withThrowingTaskGroup

Fire-and-forget

withDiscardingTaskGroup

Server/infinite loop

withDiscardingTaskGroup

Common Pitfalls

1. Forgetting That Order Is Not Guaranteed

2. Not Handling Errors Correctly

3. Creating Too Many Tasks at Once

Going Further

TaskGroup is the essential tool for dynamic parallelism in Swift. Combined with async let for simple cases and DiscardingTaskGroup for servers, you have everything you need to write performant and safe concurrent code.

Official Resources