Swift Error Handling Style Workflow
Purpose
Make Swift failure behavior clear at the call site and useful when something
breaks.
The house style is concise, typed by default for Swift-owned failure surfaces,
and functional in feel: fallible values should move through explicit carriers,
error messages should explain the failed operation, and recovery should happen
at the boundary that can actually choose a next step.
Source Check
Use repo-local guidance first. For general language behavior, prefer the Swift
Book, Swift Standard Library docs, Swift Evolution, and Apple Foundation docs:
When To Use
- Use this skill when designing or reviewing Swift error surfaces.
- Use this skill when code hides recoverable failures in
nil, strings, logs, or
broad catch-all wrappers.
- Use this skill when deciding between
throws, typed throws, Result,
Optional, AsyncSequence failure types, framework errors, or domain errors.
- Use this skill when modernizing nested
do/catch, callback-era
Result-passing, weak diagnostics, or awkward Objective-C/Cocoa error
bridging.
Workflow
- Identify the failure boundary:
- operation
- inputs
- success value
- expected absence
- recoverable failures
- programmer errors
- framework or transport errors
- async or streaming boundary
- Choose the carrier:
- nonoptional value when failure is impossible after construction
Optional when absence is expected and not diagnostic
- typed throws for Swift-owned fallible operations when the error type can be
named clearly
- untyped
throws or async throws when the operation forwards broad,
open-ended framework, filesystem, networking, database, plugin, or
dependency failures without adding a useful typed boundary
Result when success or failure must be stored, combined, cached, tested,
or delivered through a non-throwing callback
AsyncSequence failure types when values arrive over time and iteration can
fail
- existing framework errors when the platform already gives a precise error
domain
- Model domain failures:
- prefer existing framework errors until a concrete custom domain, extension,
or call-site recovery need appears
- prefer small
enum errors with associated values when the cases are closed
and meaningful
- preserve underlying errors when they help diagnosis
- use
LocalizedError for user-visible or operator-facing descriptions
- use
CustomNSError when Cocoa interop, error domains, codes, or user-info
keys matter
- use
RecoverableError only when the caller can present concrete recovery
choices
- Keep flow concise:
- use
try and try await for straight-line fallible work
- use
map, flatMap, mapError, Result.get(), and typed transforms when
the failure value is intentionally part of the pipeline
- prefer functional composition over imperative branching whenever it stays
accurate and readable
- split long chains at diagnostic, side-effect, actor, or async boundaries
- catch narrowly where recovery happens
- let errors propagate when the current layer has no useful recovery decision
- Improve diagnostics:
- include operation, source, important input identity, likely cause, and next
inspection point when the error reaches a human
- keep low-level details available without dumping secrets or raw payloads
- log at the boundary that has context, not at every propagation hop
- avoid vague messages such as
failed, invalid, or unknown error
House Defaults
- Prefer typed throws for Swift-owned synchronous and structured-concurrency
APIs when the error type can be named clearly.
- Prefer untyped
throws when forwarding broad framework, filesystem,
networking, database, plugin, or dependency failures without changing their
meaning.
- Prefer
Result for value-level composition, storage, callback interop, batch
outcomes, and tests that need to assert failure as data.
- Prefer
Optional only for ordinary absence. Do not erase useful failure
information to make a pipeline look tidy.
- Prefer existing Foundation, Cocoa, SwiftPM, SwiftNIO, Vapor, Hummingbird, or
framework error types until a concrete custom domain, extension, or recovery
need appears.
- Prefer small domain error enums over broad wrapper hierarchies when custom
errors are needed.
- Prefer preserving underlying errors over stringifying them.
- Prefer direct propagation over local catch-and-rethrow wrappers that add no new
context.
- Prefer functional transforms, narrow recovery helpers, and value-level error
composition over broad imperative branching.
- Prefer assertions, preconditions, or non-throwing validation for programmer
mistakes only when recovery is not part of the API contract.
Typed Throws Guidance
Typed throws is the preferred house style for Swift-owned error surfaces, while
untyped throws remains the right tool for open-ended failure domains.
Use typed throws when:
- the operation has a closed domain error set
- the operation is Swift-owned and the error type can be named clearly
- callers benefit from exhaustive
catch handling
- tests should assert every domain case
- a generic API should preserve its caller's failure type
- embedded, performance-sensitive, or allocation-sensitive code benefits from
carrying a concrete error type
Avoid typed throws when:
- the operation mostly forwards framework, filesystem, networking, database, or
plugin errors without adding a meaningful typed boundary
- the API boundary is public and the error set is likely to grow
- callers would immediately erase the type to
any Error
- the type annotation makes simple code noisier without changing recovery
Error Helper Direction
A small shared helper package could become useful if several repositories start
needing the same concise diagnostic, wrapping, or recovery helpers.
Treat that as a separate design decision. A future package might explore generic
helpers, variadic generics or parameter packs, and macros, but do not invent a
local helper framework inside one app or skill unless the repeated call sites
already exist and the package design has been discussed.
Use the root Socket maintainer plan at
docs/maintainers/errorhandles-package-plan.md when deciding whether that helper
belongs in Socket or in a separate Swift package repository.
Example Shapes
Straight-line fallible work:
func loadManifest(at url: URL) async throws -> Manifest {
let data = try await fetch(url)
return try ManifestDecoder().decode(data)
}
Closed domain failures:
enum ManifestError: Error, Equatable {
case missingName(URL)
case unsupportedVersion(String)
}
func validate(_ manifest: Manifest) throws(ManifestError) -> Manifest {
guard let name = manifest.name else {
throw .missingName(manifest.sourceURL)
}
guard manifest.version.isSupported else {
throw .unsupportedVersion(manifest.version.rawValue)
}
return manifest
}
Stored or batched failures:
let results: [Result<Package, PackageLoadError>] = urls.map { url in
Result { try loadPackage(at: url) }
}
let packages = results.compactMap { try? $0.get() }
let failures = results.compactMap { result -> PackageLoadError? in
guard case let .failure(error) = result else { return nil }
return error
}
Operator-facing error context:
enum PackageLoadError: LocalizedError {
case unreadableManifest(url: URL, underlying: any Error)
var errorDescription: String? {
switch self {
case let .unreadableManifest(url, underlying):
"Could not read Package.swift at \(url.path). Check that the file exists, is readable, and contains valid Swift package syntax. Underlying error: \(underlying)"
}
}
}
Output Shape
Return:
Failure state: current operation, success value, absence, recoverable
failures, and programmer errors.
Carrier choice: why throws, typed throws, Result, Optional,
AsyncSequence, existing framework errors, or domain errors fit.
House-style changes: API signatures, error types, propagation, recovery,
and diagnostics to change.
Examples: compact call-site or implementation sketch.
Validation: compile, tests, and failure-case checks needed.
Guardrails
- Do not add error abstraction layers without a real caller, recovery path, or
interop need.
- Do not wrap every underlying error just to make a local enum exhaustive.
- Do not force typed throws onto APIs whose failures are still genuinely
open-ended.
- Do not hide recoverable failures in logs,
nil, default values, or comments.
- Do not over-functionalize error handling when a narrow
do/catch is clearer.
- Do not catch only to print or log and then continue with corrupted state.
1---2name: swift-error-handling-style-workflow3description: Design or repair Swift error handling style using throws, typed throws, Result, Optional, AsyncSequence failure types, domain errors, Cocoa bridging, and concise functional recovery paths.4license: Apache-2.05---67# Swift Error Handling Style Workflow89## Purpose1011Make Swift failure behavior clear at the call site and useful when something12breaks.1314The house style is concise, typed by default for Swift-owned failure surfaces,15and functional in feel: fallible values should move through explicit carriers,16error messages should explain the failed operation, and recovery should happen17at the boundary that can actually choose a next step.1819## Source Check2021Use repo-local guidance first. For general language behavior, prefer the Swift22Book, Swift Standard Library docs, Swift Evolution, and Apple Foundation docs:2324- [Error Handling in The Swift Programming Language](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/errorhandling/)25- [SE-0413: Typed throws](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0413-typed-throws.md)26- [Result](https://developer.apple.com/documentation/swift/result)27- [About Imported Cocoa Error Parameters](https://developer.apple.com/documentation/swift/about-imported-cocoa-error-parameters)28- [Handling Cocoa Errors in Swift](https://developer.apple.com/documentation/swift/handling-cocoa-errors-in-swift)29- [LocalizedError](https://developer.apple.com/documentation/foundation/localizederror)30- [CustomNSError](https://developer.apple.com/documentation/foundation/customnserror)31- [RecoverableError](https://developer.apple.com/documentation/foundation/recoverableerror)3233## When To Use3435- Use this skill when designing or reviewing Swift error surfaces.36- Use this skill when code hides recoverable failures in `nil`, strings, logs, or37 broad catch-all wrappers.38- Use this skill when deciding between `throws`, typed throws, `Result`,39 `Optional`, `AsyncSequence` failure types, framework errors, or domain errors.40- Use this skill when modernizing nested `do`/`catch`, callback-era41 `Result`-passing, weak diagnostics, or awkward Objective-C/Cocoa error42 bridging.4344## Workflow45461. Identify the failure boundary:47 - operation48 - inputs49 - success value50 - expected absence51 - recoverable failures52 - programmer errors53 - framework or transport errors54 - async or streaming boundary552. Choose the carrier:56 - nonoptional value when failure is impossible after construction57 - `Optional` when absence is expected and not diagnostic58 - typed throws for Swift-owned fallible operations when the error type can be59 named clearly60 - untyped `throws` or `async throws` when the operation forwards broad,61 open-ended framework, filesystem, networking, database, plugin, or62 dependency failures without adding a useful typed boundary63 - `Result` when success or failure must be stored, combined, cached, tested,64 or delivered through a non-throwing callback65 - `AsyncSequence` failure types when values arrive over time and iteration can66 fail67 - existing framework errors when the platform already gives a precise error68 domain693. Model domain failures:70 - prefer existing framework errors until a concrete custom domain, extension,71 or call-site recovery need appears72 - prefer small `enum` errors with associated values when the cases are closed73 and meaningful74 - preserve underlying errors when they help diagnosis75 - use `LocalizedError` for user-visible or operator-facing descriptions76 - use `CustomNSError` when Cocoa interop, error domains, codes, or user-info77 keys matter78 - use `RecoverableError` only when the caller can present concrete recovery79 choices804. Keep flow concise:81 - use `try` and `try await` for straight-line fallible work82 - use `map`, `flatMap`, `mapError`, `Result.get()`, and typed transforms when83 the failure value is intentionally part of the pipeline84 - prefer functional composition over imperative branching whenever it stays85 accurate and readable86 - split long chains at diagnostic, side-effect, actor, or async boundaries87 - catch narrowly where recovery happens88 - let errors propagate when the current layer has no useful recovery decision895. Improve diagnostics:90 - include operation, source, important input identity, likely cause, and next91 inspection point when the error reaches a human92 - keep low-level details available without dumping secrets or raw payloads93 - log at the boundary that has context, not at every propagation hop94 - avoid vague messages such as `failed`, `invalid`, or `unknown error`9596## House Defaults9798- Prefer typed throws for Swift-owned synchronous and structured-concurrency99 APIs when the error type can be named clearly.100- Prefer untyped `throws` when forwarding broad framework, filesystem,101 networking, database, plugin, or dependency failures without changing their102 meaning.103- Prefer `Result` for value-level composition, storage, callback interop, batch104 outcomes, and tests that need to assert failure as data.105- Prefer `Optional` only for ordinary absence. Do not erase useful failure106 information to make a pipeline look tidy.107- Prefer existing Foundation, Cocoa, SwiftPM, SwiftNIO, Vapor, Hummingbird, or108 framework error types until a concrete custom domain, extension, or recovery109 need appears.110- Prefer small domain error enums over broad wrapper hierarchies when custom111 errors are needed.112- Prefer preserving underlying errors over stringifying them.113- Prefer direct propagation over local catch-and-rethrow wrappers that add no new114 context.115- Prefer functional transforms, narrow recovery helpers, and value-level error116 composition over broad imperative branching.117- Prefer assertions, preconditions, or non-throwing validation for programmer118 mistakes only when recovery is not part of the API contract.119120## Typed Throws Guidance121122Typed throws is the preferred house style for Swift-owned error surfaces, while123untyped `throws` remains the right tool for open-ended failure domains.124125Use typed throws when:126127- the operation has a closed domain error set128- the operation is Swift-owned and the error type can be named clearly129- callers benefit from exhaustive `catch` handling130- tests should assert every domain case131- a generic API should preserve its caller's failure type132- embedded, performance-sensitive, or allocation-sensitive code benefits from133 carrying a concrete error type134135Avoid typed throws when:136137- the operation mostly forwards framework, filesystem, networking, database, or138 plugin errors without adding a meaningful typed boundary139- the API boundary is public and the error set is likely to grow140- callers would immediately erase the type to `any Error`141- the type annotation makes simple code noisier without changing recovery142143## Error Helper Direction144145A small shared helper package could become useful if several repositories start146needing the same concise diagnostic, wrapping, or recovery helpers.147148Treat that as a separate design decision. A future package might explore generic149helpers, variadic generics or parameter packs, and macros, but do not invent a150local helper framework inside one app or skill unless the repeated call sites151already exist and the package design has been discussed.152153Use the root Socket maintainer plan at154`docs/maintainers/errorhandles-package-plan.md` when deciding whether that helper155belongs in Socket or in a separate Swift package repository.156157## Example Shapes158159Straight-line fallible work:160161```swift162func loadManifest(at url: URL) async throws -> Manifest {163 let data = try await fetch(url)164 return try ManifestDecoder().decode(data)165}166```167168Closed domain failures:169170```swift171enum ManifestError: Error, Equatable {172 case missingName(URL)173 case unsupportedVersion(String)174}175176func validate(_ manifest: Manifest) throws(ManifestError) -> Manifest {177 guard let name = manifest.name else {178 throw .missingName(manifest.sourceURL)179 }180181 guard manifest.version.isSupported else {182 throw .unsupportedVersion(manifest.version.rawValue)183 }184185 return manifest186}187```188189Stored or batched failures:190191```swift192let results: [Result<Package, PackageLoadError>] = urls.map { url in193 Result { try loadPackage(at: url) }194}195196let packages = results.compactMap { try? $0.get() }197let failures = results.compactMap { result -> PackageLoadError? in198 guard case let .failure(error) = result else { return nil }199 return error200}201```202203Operator-facing error context:204205```swift206enum PackageLoadError: LocalizedError {207 case unreadableManifest(url: URL, underlying: any Error)208209 var errorDescription: String? {210 switch self {211 case let .unreadableManifest(url, underlying):212 "Could not read Package.swift at \(url.path). Check that the file exists, is readable, and contains valid Swift package syntax. Underlying error: \(underlying)"213 }214 }215}216```217218## Output Shape219220Return:2212221. `Failure state`: current operation, success value, absence, recoverable223 failures, and programmer errors.2242. `Carrier choice`: why `throws`, typed throws, `Result`, `Optional`,225 `AsyncSequence`, existing framework errors, or domain errors fit.2263. `House-style changes`: API signatures, error types, propagation, recovery,227 and diagnostics to change.2284. `Examples`: compact call-site or implementation sketch.2295. `Validation`: compile, tests, and failure-case checks needed.230231## Guardrails232233- Do not add error abstraction layers without a real caller, recovery path, or234 interop need.235- Do not wrap every underlying error just to make a local enum exhaustive.236- Do not force typed throws onto APIs whose failures are still genuinely237 open-ended.238- Do not hide recoverable failures in logs, `nil`, default values, or comments.239- Do not over-functionalize error handling when a narrow `do`/`catch` is clearer.240- Do not catch only to print or log and then continue with corrupted state.