To preserve typed errors across concurrent Swift calculations, return a typed Result from each child task and unwrap it after collection. This Mac example calculates synthetic rectangle pixel counts while keeping input order, explicit failures, and an all-or-error batch contract.
Why use typed results at the task-group boundary?
Suppose you are building a desktop graphics preview that calculates pixel counts for several rectangles. Each calculation can reject invalid dimensions or report integer overflow. The caller needs a complete ordered array or a specific calculation error, so it can handle every supported failure explicitly.
Swift introduced typed throws in Swift 6.0. A declaration such as throws(CalculationError) limits the function’s thrown values to that error type. This is useful for a small, closed calculation contract whose implementation and callers evolve together. See SE-0413: Typed throws.
The concurrency API boundary matters. Apple’s documented withThrowingTaskGroup signature supplies a ThrowingTaskGroup<ChildTaskResult, any Error> to its body. Its generic-looking group type does not provide a concrete domain-error parameter through that factory. Therefore, adding typed throws to your calculation alone does not establish a concrete thrown type throughout this group API. See withThrowingTaskGroup.
A supported route is to use withTaskGroup and make each child return Result<Int, CalculationError>. The task completes normally with a value representing success or failure. After collecting those values, the batch function calls get(), whose documented signature is throws(Failure). That restores typed throwing without a runtime cast. See Apple’s Result documentation and get() documentation.
Define the batch contract before writing concurrency code
For this example, each input contains an integer width and height measured in pixels. The calculation is their product. Everything is synthetic and stays in memory; the program neither renders an image nor loads or saves application data.
The following policies are design choices for this example. They make both the returned values and the selected error predictable:
| Situation | Batch behavior |
|---|---|
| Every rectangle is valid | Return pixel counts in input order |
| A width or height is zero or negative | Report that rectangle as invalid |
A product cannot fit in Int |
Report overflow for that rectangle |
| Several rectangles fail | Report the failure with the lowest input index |
| Cancellation is observed at a batch checkpoint | Report .cancelled |
| The input array is empty | Return an empty array, unless cancellation is observed |
Selecting the lowest input index requires collecting every child result before choosing a calculation failure. Swift does not guarantee task-group scheduling order, and completion order can differ from submission order. Attaching an index to each result lets the collector restore the intended order. See TaskGroup.
This policy suits a small preview batch where repeatable feedback matters. It deliberately waits for all calculations. A design that reports the first failure observed during execution would make a different tradeoff and could select a different rectangle across runs.
Build a complete local example on your Mac
Use an installed Swift 6 or newer toolchain. In Terminal, check it with swift --version. Swift.org documents that check in Getting Started and provides Mac installation instructions in Install Swift — macOS.
Create a small executable package using the workflow documented on the installation page:
mkdir TypedCalculations
cd TypedCalculations
swift package init --name TypedCalculations --type executable
Replace the generated Swift entry file under Sources with the following three code blocks, in order, as one file. Keep just one executable entry point. This example uses the package’s ordinary default actor isolation; a project configured with default main-actor isolation needs its calculation types and functions placed outside that isolation.
Define inputs and concrete failures
The error enum preserves structured information: the case identifies the problem, and its associated index identifies the input. Foundation’s LocalizedError protocol supplies an optional errorDescription for presentation. The English messages below are sample strings; conforming to the protocol does not translate them automatically. See LocalizedError.
import Foundation
struct Rectangle: Sendable {
let width: Int
let height: Int
}
enum CalculationError: Error, Sendable, LocalizedError {
case invalidDimensions(index: Int)
case overflow(index: Int)
case cancelled
var errorDescription: String? {
switch self {
case .invalidDimensions(let index):
return "Rectangle \(index + 1) needs positive dimensions."
case .overflow(let index):
return "Rectangle \(index + 1) has too many pixels for Int."
case .cancelled:
return "Calculation cancelled."
}
}
}
struct IndexedOutcome: Sendable {
let index: Int
let result: Result<Int, CalculationError>
}
Calculate, collect, and restore typed throwing
The arithmetic uses multipliedReportingOverflow(by:). Apple documents that its partialValue is the full product only when overflow is false; otherwise, it contains a truncated product. The guard below prevents that truncated value from becoming a successful pixel count. See multipliedReportingOverflow(by:).
func pixelCount(
for rectangle: Rectangle,
index: Int
) throws(CalculationError) -> Int {
if Task.isCancelled {
throw .cancelled
}
guard rectangle.width > 0, rectangle.height > 0 else {
throw .invalidDimensions(index: index)
}
let product = rectangle.width.multipliedReportingOverflow(
by: rectangle.height
)
guard !product.overflow else {
throw .overflow(index: index)
}
return product.partialValue
}
func calculateBatch(
_ rectangles: [Rectangle]
) async throws(CalculationError) -> [Int] {
if Task.isCancelled {
throw .cancelled
}
let outcomes: [IndexedOutcome] = await withTaskGroup(
of: IndexedOutcome.self,
returning: [IndexedOutcome].self
) { group in
for (index, rectangle) in rectangles.enumerated() {
group.addTask {
let result = Result<Int, CalculationError> {
() throws(CalculationError) -> Int in
try pixelCount(for: rectangle, index: index)
}
return IndexedOutcome(index: index, result: result)
}
}
var collected: [IndexedOutcome] = []
for await outcome in group {
collected.append(outcome)
}
return collected.sorted { $0.index < $1.index }
}
if Task.isCancelled {
throw .cancelled
}
var counts: [Int] = []
counts.reserveCapacity(outcomes.count)
for outcome in outcomes {
counts.append(try outcome.result.get())
}
return counts
}
The explicit throws(CalculationError) annotation on the Result closure keeps its error contract visible rather than relying on closure inference. SE-0413 notes that its originally planned full typed-throws inference changes were not implemented in Swift 6.0. See the implementation note in Typed throws.
Only the parent collector appends to collected. Each child receives a rectangle and an index and returns its own Sendable outcome. This matches the task-group requirement that child result values be Sendable; the nonthrowing group body collects them with for await. See withTaskGroup.
Add an executable entry point
This final block supplies all sample inputs and calls the batch function. The explicit assignment inside catch also checks the intended API contract at compile time: the caught value must be a CalculationError.
@main
struct TypedCalculations {
static func main() async {
let examples: [(name: String, rectangles: [Rectangle])] = [
(
name: "Valid",
rectangles: [
Rectangle(width: 640, height: 480),
Rectangle(width: 320, height: 200)
]
),
(
name: "Multiple failures",
rectangles: [
Rectangle(width: 0, height: 20),
Rectangle(width: Int.max, height: 2)
]
),
(
name: "Overflow",
rectangles: [
Rectangle(width: Int.max, height: 2)
]
),
(name: "Empty", rectangles: [])
]
for example in examples {
do {
let counts = try await calculateBatch(example.rectangles)
print("\(example.name): \(counts)")
} catch {
let failure: CalculationError = error
print("\(example.name): \(failure.errorDescription ?? "Calculation failed.")")
}
}
}
}
Run the executable from the package directory:
swift run TypedCalculations
The expected application output, excluding build messages, is shown below. No Swift compiler was available in the research environment, so this program was inspected against the retrieved API documentation but was not compiled or executed there.
Valid: [307200, 64000]
Multiple failures: Rectangle 1 needs positive dimensions.
Overflow: Rectangle 1 has too many pixels for Int.
Empty: []
The valid counts follow directly from the illustrative inputs: 640 multiplied by 480 is 307,200, and 320 multiplied by 200 is 64,000. In the multiple-failure example, the first rectangle wins because results are inspected in input order, even if the second child finishes earlier.
How do cancellation and partial results behave?
Cancellation is cooperative in Swift. A task must observe cancellation and respond; requesting cancellation does not automatically interrupt its calculation. Structured task groups also wait for their children to finish before returning. See the Swift language guide’s Concurrency chapter and Apple’s withTaskGroup documentation.
Here, Task.isCancelled is checked before submission, inside each calculation, and after collection. When a checkpoint observes cancellation, the code represents it using the existing .cancelled case. Reading the Boolean avoids introducing another thrown error type into this closed contract. Apple documents the property in Task.isCancelled.
The post-collection check gives observed batch cancellation precedence over rectangle failures. These checks are checkpoints, not an atomic guarantee: cancellation requested after the final check might not affect the returned result. For a longer calculation containing repeated work, add checks within that work so cancellation can be observed sooner.
A failed child does not trigger cancellation in this implementation. It returns a normal Result.failure value, and the collector continues. If every child finishes successfully, the caller receives every count. If unwrapping encounters a failure, the function throws and its local, partially assembled counts array is never returned.
If the desktop interface instead needs a successful or failed status for every rectangle, change the public batch contract to return the ordered outcomes. Keep each failure attached to its index. That is a different reader-facing behavior from the all-or-error array implemented here.
Diagnose a lost error type without hiding failures
If your adapted code stops compiling, trace the type across each boundary in this order:
- Check that the calculation declares
throws(CalculationError). - Check that the child value is
Result<Int, CalculationError>, rather than a result whose failure isany Error. - Keep the closure’s explicit typed-throws annotation if inference broadens its error type.
- Use nonthrowing
withTaskGroupfor transporting these result values. - Unwrap with
get()inside the typed batch function. - Check whether any newly added throwing operation introduces a different error type.
A forced cast cannot prove this contract. Likewise, discarding failed results or substituting zero would change the meaning of the calculation. Preserve failures as values until the point where your chosen batch policy decides how to report them.
For these tiny multiplications, concurrency is an architectural demonstration; no performance improvement is claimed. The useful outcome is a precise contract: successful counts retain input order, multiple failures have a deterministic selection rule, and callers receive a concrete error enum they can handle directly.
Sources
Typed throws — SE-0413 — Swift Project.
withThrowingTaskGroup(of:returning:isolation:body:) — Apple.
Result — Apple.
get() — Apple.
TaskGroup — Apple.
Getting Started — Swift.org.
Install Swift — macOS — Swift.org.
LocalizedError — Apple.
multipliedReportingOverflow(by:) — Apple.
Concurrency — The Swift Programming Language — Swift Project.
isCancelled — Apple.