Article

Task Group Patterns

Bitesize

Task groups let you run several async jobs in parallel and collect the results together.

let values = await withTaskGroup(of: Int.self, returning: [Int].self) { group in
    group.addTask { 1 }
    group.addTask { 2 }

    var results: [Int] = []
    for await value in group {
        results.append(value)
    }
    return results
}

Use a task group when the work items are independent and can happen at the same time.

More detail

A task group diagram showing work fanning out and results gathering back

Task groups are part of Swift’s structured concurrency model. That means the child tasks belong to a clear parent scope.

This gives you useful behavior by default:

  • cancellation flows down
  • thrown errors can be handled centrally
  • the lifetime of child work stays bounded

A common case is fetching multiple pieces of data for one screen and then combining them afterward.

That parent-child relationship is what makes task groups feel disciplined. You know where the work starts, where it ends, and who is responsible for handling the results. That is a big step up from unstructured task spawning.

It also encourages you to think in terms of batches of related work. If several operations contribute to one higher-level result, a task group can describe that relationship directly instead of scattering the logic across multiple call sites.

As a result, task groups often make async code more readable as well as faster. The concurrency becomes part of the shape of the function rather than something hidden in side effects.

Deep dive

The biggest advantage of task groups is not just speed. It is control.

Unstructured concurrency can make it easy to lose track of who owns a task, when it should stop, and where errors should go. Task groups keep that relationship explicit.

They are best when the parent operation is responsible for the whole batch. If the work items are truly tied to a single result, groups usually produce cleaner and safer concurrency than scattered detached tasks.

Another strength is incremental result handling. You do not have to wait for every child to finish before doing anything. Iterating the group lets you respond as work completes, which can be useful for aggregation, ranking, or early partial progress.

There is also a design benefit in deciding whether the group should be throwing, non-throwing, or bounded to a particular output shape. That forces the parent operation to define how resilient or strict the batch should be.

Task groups also make cancellation semantics much easier to reason about. When the parent scope ends or fails, the child work is not left floating around detached from the rest of the program’s logic.

This is what structured concurrency is really buying you. It is not just syntax for parallelism. It is a way of saying that the concurrent work belongs to a coherent unit of responsibility.

Once you start seeing task groups that way, they become a design tool for organizing asynchronous operations, not just a performance trick.

Finished the deep dive?

You made it to the end.

Mark this article as read once you have worked through the full piece. It is a small way to keep track of what you have genuinely finished.

More in this area

Keep the thread going.

Jump sideways into the related ideas that sit closest to this piece and keep the same mental context alive.

  1. 01 Concurrency Explore topic