Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A Swift Task represents one unit of asynchronous work; a task group creates child tasks whose lifetimes are bounded by a scope. Keep a task handle if you need to await its result or request cancellation. Treat cancellation as a signal that work should stop—not as a command that forcibly interrupts it.

What is a Swift Task?

Task represents an asynchronous operation that can run concurrently with other work. A task may begin soon after it is created. The handle returned by Task { ... } lets you await the task’s value or request cancellation:

let task = Task {
    await loadProfile()
}

let profile = await task.value

Keep the handle for as long as the caller needs those controls. Discarding it does not cancel the task; the work can continue without the caller retaining a way to await it or request cancellation through that handle. Use a task group instead when child work should be structurally contained in a particular operation.

How does Swift task cancellation work?

Cancellation is cooperative. Apple describes the distinction this way: “Tasks include a shared mechanism for indicating cancellation, but not a shared implementation for how to handle cancellation.” Calling cancel() changes the task’s cancellation state; it does not forcibly stop arbitrary code.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Check at meaningful stopping points

Cancellation is useful only when the task’s code checks for it and responds. In a throwing operation, Task.checkCancellation() throws when cancellation has been requested:

func processItems() async throws {
    for item in items {
        try Task.checkCancellation()
        try await process(item)
    }
}

For nonthrowing work, inspect Task.isCancelled and choose an appropriate response, such as returning or skipping optional work:

func refreshIfNeeded() async {
    guard !Task.isCancelled else { return }
    await refresh()
}

Choose checks where stopping is safe and useful: for example, between items or before beginning an expensive next stage. If work has cleanup or partial results to handle, make that part of the cancellation path.

What cancellation does—and does not—guarantee

A cancellation request does not by itself interrupt a long-running synchronous loop or stop a child operation that never checks for cancellation. Code and APIs involved in the operation must cooperate. A cancelled task can therefore continue until it reaches code that observes cancellation or otherwise finishes.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When should you use a task group?

Apple calls a task group “the primary way to create structured concurrency tasks in Swift.” Use withTaskGroup for a dynamically sized set of child operations that do not report failures by throwing. Use withThrowingTaskGroup when child operations report failures by throwing. In both cases, child lifetimes stay within the group’s closure.

Nonthrowing children: withTaskGroup

This example starts one child for each URL and collects each child’s result as it completes:

func fetchAll(_ urls: [URL]) async -> [String] {
    await withTaskGroup(of: String.self) { group in
        for url in urls {
            group.addTask {
                await fetchText(from: url)
            }
        }

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

The child operations run concurrently, but their scheduling and completion order are unspecified. In this example, results reflects the order values arrive from the children, not necessarily the order of urls.

Throwing children: withThrowingTaskGroup

Use the throwing variant when a child can fail by throwing. The body must handle the throwing iteration with try:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
func fetchAll(_ urls: [URL]) async throws -> [String] {
    try await withThrowingTaskGroup(of: String.self) { group in
        for url in urls {
            group.addTask {
                try await fetchText(from: url)
            }
        }

        var results: [String] = []
        for try await text in group {
            results.append(text)
        }
        return results
    }
}

Choose between the two group types according to how child operations report errors. Neither variant promises that children execute or finish in the order they were added.

Does cancelAll() stop group work immediately?

No. cancelAll() signals cancellation to the group’s child tasks; their code still has to observe and honor that signal. It is not a kill switch for arbitrary work.

A group’s scope also waits for all of its children before returning. That remains true if the body has already received an early result and called cancelAll(): outstanding children must finish or respond to cancellation before the group scope exits. Make child operations cancellation-aware if you want them to wind down promptly.

When adding work after cancellation is possible, addTaskUnlessCancelled lets code avoid creating a new child if the group is already cancelled. This prevents new work from being started in that case; it does not stop children that are already running.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

How do you preserve result order?

Task-group children execute concurrently and may complete in any order. If output must correspond to input order, carry an index or other identifier with each result, then reorder after collecting. For example, a child can return (index, value); store results by index or sort them before returning. If the caller does not care about order, consume results as they arrive rather than adding ordering machinery.

When is a discarding task group useful?

DiscardingTaskGroup suits fire-and-process work when completed child values do not need to be consumed later. Apple documents that it eagerly discards and releases completed child tasks, reducing the amount of completed task-result data retained. Choose it when that result-retention behavior matches the job; use an ordinary task group when the caller needs to collect child results.

What does Swift 6 change for concurrency?

Swift 6 language mode and strict concurrency checking are migration and diagnostics tools: Apple says strict checking can find data races at compile time, and Xcode 16 supports Swift 6 language mode. These checks help surface unsafe concurrent access during development; they do not establish that every runtime concurrency problem has been eliminated. Review diagnostics and make shared-state access safe rather than treating a clean compile as a substitute for sound task design.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.