To keep successful Swift calculations when sibling tasks fail, catch each calculation’s error inside its child task and return an outcome value. This Mac command-line example collects every outcome, preserves input order, and keeps successful values available.

The task is a small, synthetic batch calculation: square a list of nonnegative integers and report which inputs succeeded. Negative inputs and integer overflow produce individual failures. All calculation data stays in memory; the program prints its results in Terminal.

Why can one error interrupt result collection?

A throwing child task does not immediately cancel its siblings. The consequential boundary is the task-group body: when an error escapes that body, Swift cancels the remaining children. Apple documents this distinction in its reference for withThrowingTaskGroup(of:returning:isolation:body:).

Consequently, a throwing iteration loop with an outer catch can stop collecting at the first observed error. Earlier values may already be stored, but the loop does not resume after that catch. Remaining children receive cancellation if the error escapes the group body.

For independent calculations, make the failure policy explicit. The example below uses a nonthrowing task group whose children return success, failure, or cancellation. The calculation itself still throws; its wrapper converts that error into a value before it reaches the group.

This fits Swift’s documented boundary: children in a nonthrowing task group cannot propagate thrown errors, so they must handle them internally or represent their outcome as a return value. See Apple’s TaskGroup documentation.

Define what each input must return

The batch contract is one outcome per input position. A successful calculation returns an exact integer square. A rejected calculation returns a failure description. A calculation skipped because cancellation was observed returns a cancellation outcome.

Situation Outcome Batch behavior
Nonnegative input whose square fits in Int64 Success containing the square Retain the value
Negative input Failure: negativeInput Continue collecting other outcomes
Square exceeds the Int64 range Failure: overflow Continue collecting other outcomes
Cancellation observed before calculation Cancelled Retain completed outcomes and skip pending work

Rejecting negative inputs is an illustrative application rule; negative numbers can be squared mathematically. Keeping that rule separate from overflow makes the sample demonstrate two distinct failure conditions.

For multiplication, use multipliedReportingOverflow(by:) and inspect its overflow flag. Apple specifies that the returned partial value is the complete product only when that flag is false; otherwise it is truncated. The example discards that truncated value and throws an error. See Int64.multipliedReportingOverflow(by:).

Every outcome also carries its input index. Swift does not guarantee child-task completion order, so appending completed values alone cannot preserve the original order. The language guide describes this scheduling behavior in Concurrency.

Run the complete example on your Mac

Use a Mac with a Swift 6 or later toolchain. In Terminal, run swift --version to check the installed compiler, as recommended by Swift.org’s Getting Started guide. If Swift is unavailable, follow the installation link in that guide before continuing.

Save the following complete program as Calculations.swift in a working folder. It imports Foundation to choose an initial concurrency limit from the active processor count.

import Foundation

enum CalculationError: Error {
    case negativeInput
    case overflow
}

enum Outcome: Sendable {
    case success(Int64)
    case failure(String)
    case cancelled
}

struct Row: Sendable {
    let index: Int
    let outcome: Outcome
}

func square(_ input: Int64) throws -> Int64 {
    guard input >= 0 else {
        throw CalculationError.negativeInput
    }
    let product = input.multipliedReportingOverflow(by: input)
    guard !product.overflow else {
        throw CalculationError.overflow
    }
    return product.partialValue
}

func evaluate(index: Int, input: Int64) -> Row {
    do {
        try Task.checkCancellation()
        let value = try square(input)
        return Row(index: index, outcome: .success(value))
    } catch is CancellationError {
        return Row(index: index, outcome: .cancelled)
    } catch {
        return Row(
            index: index,
            outcome: .failure(String(describing: error))
        )
    }
}

func collect(
    _ inputs: [Int64],
    maxConcurrent: Int
) async -> [Row] {
    guard !inputs.isEmpty else { return [] }
    let width = min(inputs.count, max(1, maxConcurrent))

    return await withTaskGroup(of: Row.self) { group in
        var rows: [Row] = []
        rows.reserveCapacity(inputs.count)

        for index in 0..<width {
            let input = inputs[index]
            group.addTask {
                evaluate(index: index, input: input)
            }
        }
        var nextIndex = width

        while let row = await group.next() {
            rows.append(row)

            if Task.isCancelled {
                while nextIndex < inputs.count {
                    rows.append(Row(
                        index: nextIndex,
                        outcome: .cancelled
                    ))
                    nextIndex += 1
                }
            } else if nextIndex < inputs.count {
                let index = nextIndex
                let input = inputs[index]
                nextIndex += 1
                group.addTask {
                    evaluate(index: index, input: input)
                }
            }
        }

        return rows.sorted { $0.index < $1.index }
    }
}

@main
struct CalculationDemo {
    static func main() async {
        let inputs: [Int64] = [3, -2, 4, Int64.max, 0]
        let width = min(
            4,
            max(1, ProcessInfo.processInfo.activeProcessorCount)
        )
        let rows = await collect(inputs, maxConcurrent: width)
        var successes: [Int64] = []

        for row in rows {
            let input = inputs[row.index]
            switch row.outcome {
            case .success(let value):
                successes.append(value)
                print("[\(row.index)] \(input): success \(value)")
            case .failure(let message):
                print("[\(row.index)] \(input): failure \(message)")
            case .cancelled:
                print("[\(row.index)] \(input): cancelled")
            }
        }
        print("Successful values: \(successes)")
    }
}

From that folder, compile and run the program:

swiftc -swift-version 6 -parse-as-library Calculations.swift -o Calculations
./Calculations

The -parse-as-library option lets this single source file use its explicit @main entry point. The executable performs the calculations without reading or writing application data. Swift was unavailable in the research environment, so this program was inspected but was not compiled or executed there.

For the supplied inputs without cancellation, the expected output is:

[0] 3: success 9
[1] -2: failure negativeInput
[2] 4: success 16
[3] 9223372036854775807: failure overflow
[4] 0: success 0
Successful values: [9, 16, 0]

The printed order comes from sorting by input index after collection. Execution order can vary without changing this presentation. The successful-values array is built afterward, leaving the full outcome report available for inspection.

How does the collector retain every successful result?

The parent task owns the results array. Each child captures one index and one immutable input, calculates its outcome, and returns a row. Children never append directly to the parent’s array. This follows the language guide’s approach of accumulating child results inside the task-group body. See Concurrency.

The collector initially submits a limited number of children. Each completed row frees one position for another input. A failed calculation therefore advances the batch in the same way as a successful one: both return an ordinary row, and collection continues.

Foundation’s activeProcessorCount reports the number of active processing cores available on the computer. The sample caps that number at four as an illustrative scheduling choice. That cap is not a measured optimum or a promise that four calculations run simultaneously. See Apple’s activeProcessorCount documentation.

The concurrency limit bounds outstanding child tasks, while the results array still grows with the number of inputs. This is suitable for a small desktop batch. For these tiny squares, the example demonstrates outcome handling; it makes no performance claim over a sequential loop.

The empty-input guard returns an empty array. A zero or negative concurrency limit is clamped to one. Duplicate input values remain separate because identity comes from position, so two equal inputs still produce two rows.

What happens when the parent task is cancelled?

Swift cancellation is cooperative. Cancelling a parent task propagates cancellation to its structured children, but a child must observe that signal and respond. Apple’s TaskGroup documentation explains both propagation and the requirement for cooperative handling.

Here, evaluate calls Task.checkCancellation() before calculating. Apple documents that this method throws CancellationError when cancellation is observed; the wrapper catches it separately and returns a cancelled row. See checkCancellation().

Once the collector observes cancellation, it marks inputs that have not yet been submitted as cancelled. Already submitted children are still drained. Their successful results remain in the report, and children that observe cancellation return cancelled rows.

Cancellation can arrive after a child’s check. That child may still complete successfully, which this contract accepts. If the calculation becomes a long loop, add cancellation checks at useful intervals inside that calculation. The group still waits for all submitted children to finish before returning, as documented for TaskGroup.

When should you use a throwing task group instead?

If existing children already throw and you want to preserve that interface, a throwing task group also supports collecting individual outcomes. Repeatedly call nextResult() and switch over each returned success or failure until the optional result is empty. Unlike next(), this method returns a Result value rather than throwing the child’s error to the caller. Apple documents its completion-order behavior in nextResult().

For that design, include input identity in successful values and in any errors that need attribution. A bare arithmetic error does not identify which of several inputs produced it. Also choose an explicit cancellation policy rather than treating cancellation as an ordinary calculation failure.

Use the outcome-returning approach when a complete per-input report is the main requirement. Use a throwing group when propagating an error out of the group is the desired batch behavior, or when consuming existing throwing operations through nextResult() is more convenient.

For this local calculation task, the essential contract is straightforward: retain each successful value, record each failed input, and keep collecting. Carrying an input index through every outcome makes the final report useful even when calculations finish in an unpredictable order.

Sources