Error Handling
Choosing between `throws`, `Result`, and optionals; the `try` variants, `rethrows`, typed throws, and designing error types a UI can render.
8 questions
JuniorTheoryVery commonHow does a thrown error travel up the call stack through do/catch, and what is Error really?
How does a thrown error travel up the call stack through do/catch, and what is Error really?
A throw unwinds the call and propagates to the nearest enclosing catch; each function must be marked throws or catch it. Error is an empty protocol any type can conform to — usually an enum — and conforming makes a value throwable.
Common mistakes
- ✗Thinking
Erroris a rich base class rather than an empty protocol - ✗Believing intermediate functions need no
throwsfor an error to pass - ✗Assuming only classes, not enums or structs, can be thrown
Follow-up questions
- →Why must every function on the error's path be marked
throws? - →How does a typed
catchpattern pick one error case over another?
JuniorTheoryVery commonWhat do try, try?, and try! each do to a thrown error and to the return type?
What do try, try?, and try! each do to a thrown error and to the return type?
try propagates the error to an enclosing do/catch, keeping the return type. try? swallows the error and makes the result an optional, nil on failure. try! asserts no error is thrown and traps at runtime if one is, giving the value.
Common mistakes
- ✗Thinking
try?returns aResultrather than an optional - ✗Believing
try!returnsnilon failure instead of trapping - ✗Assuming a plain
trycatches or swallows the error itself
Follow-up questions
- →When is
try?the wrong choice because the caller needs the cause? - →What runtime guarantee are you making when you write
try!?
MiddleTheoryCommonHow does an error leave an async throws function, and what happens to a Task that throws with nobody awaiting it?
How does an error leave an async throws function, and what happens to a Task that throws with nobody awaiting it?
An async throws function propagates like a synchronous throwing one; the caller writes try await and the error surfaces there. A Task stores its thrown error in a Result, read via task.value. Unawaited, that error is silently discarded.
Common mistakes
- ✗Believing an unawaited throwing
Taskcrashes or logs instead of discarding the error - ✗Forgetting the
tryintry awaitwhen calling anasync throwsfunction - ✗Thinking async errors auto-propagate to a global handler with no await
Follow-up questions
- →How does a
TaskGroupsurface a child task's thrown error? - →Why does an unawaited
Taskswallow its error rather than propagate it?
MiddleCodeCommonWrite a failable init? and a throwing init for one parsing type — when is each the right choice?
Write a failable init? and a throwing init for one parsing type — when is each the right choice?
Use init? when failure has one uninteresting cause and nil says enough; the caller just checks for nil. Use a throwing init when the caller needs the reason. init? discards the cause; throws carries a rich, propagating error.
Common mistakes
- ✗Thinking
init?can hand back the failure reason the waythrowsdoes - ✗Believing a throwing
initreturnsnilrather than propagating an error - ✗Assuming failable initializers are class-only and unusable on value types
Follow-up questions
- →Which of the two composes better with
try?at the call site? - →When does returning
nillose information the caller genuinely needs?
MiddleTheoryCommonthrows vs Result vs an optional return — when do you pick each, and what does each cost the caller?
throws vs Result vs an optional return — when do you pick each, and what does each cost the caller?
Use throws for a recoverable error propagated with try; Result to carry success-or-failure across a callback or stored boundary; an optional when the failure's cause is irrelevant. The caller pays a try, a switch, or the lost reason.
Common mistakes
- ✗Treating
Resultandthrowsas interchangeable rather than fitting sync vs stored/async flows - ✗Using an optional for a failure whose cause the caller actually needs to report
- ✗Thinking an optional return conveys an error type the way
throwsandResultdo
Follow-up questions
- →When does
Resultread better thanthrowsfor a completion-handler API? - →How do you convert a
throwscall into aResultwithout losing the error type?
MiddleDesignOccasionalAn error crosses a module boundary. Your framework calls a lower-level dependency — a networking library, a parser — that throws its own error types, and a consumer of your framework catches errors from your public API. For each failure you must decide whether to expose the underlying error to the consumer as-is, or wrap it in your own error type and rethrow. Exposing it leaks the dependency into your public API — swap the parser and every consumer's catch breaks. Wrapping it hides useful diagnostic detail unless you preserve the underlying cause. Design the module's error boundary: which errors you wrap versus pass through, how you keep the public error type stable as internals change, how you preserve the original cause for debugging, and how a consumer tells a recoverable failure apart from a programmer error across that boundary.
An error crosses a module boundary. Your framework calls a lower-level dependency — a networking library, a parser — that throws its own error types, and a consumer of your framework catches errors from your public API. For each failure you must decide whether to expose the underlying error to the consumer as-is, or wrap it in your own error type and rethrow. Exposing it leaks the dependency into your public API — swap the parser and every consumer's catch breaks. Wrapping it hides useful diagnostic detail unless you preserve the underlying cause. Design the module's error boundary: which errors you wrap versus pass through, how you keep the public error type stable as internals change, how you preserve the original cause for debugging, and how a consumer tells a recoverable failure apart from a programmer error across that boundary.
Wrap dependency errors in a module-owned type so the public API leaks no third-party error; consumers' catch survives swapping a library. Keep the original as the underlying cause. Model recoverable failures distinct from programmer errors.
Common mistakes
- ✗Leaking a dependency's concrete error type through the public API
- ✗Wrapping errors but dropping the underlying cause needed for debugging
- ✗Treating recoverable failures and programmer errors as the same case
Follow-up questions
- →How do you preserve the underlying error while presenting your own type?
- →What breaks for consumers if a public error enum is not frozen?
MiddleCodeOccasionalWrite a higher-order function that throws only when its closure throws
Write a higher-order function that throws only when its closure throws
rethrows marks a function that throws only when a closure it calls throws. With a non-throwing closure the caller invokes it without try; with a throwing one the call becomes throwing. Plain throws would force try on every caller.
Common mistakes
- ✗Thinking
rethrowscatches or swallows the closure's error - ✗Believing a
rethrowsfunction forcestryeven with a non-throwing closure - ✗Confusing
rethrowswith retrying the operation on failure
Follow-up questions
- →Why can a
rethrowsfunction not throw an error of its own? - →How does the caller's
tryrequirement change with the closure passed in?
SeniorDesignOccasionalYou are designing the error model for a networked iOS app. It has a transport layer built on Apple's networking API URLSession plus decoding, a domain layer with business rules such as insufficient funds, and a presentation layer built with the declarative-UI framework SwiftUI. Requests fail for many reasons — no connectivity, a 500, a malformed payload, an expired token, a declined operation. Product wants every failure to reach the user as a clear, localized message with an optional retry, while engineers want full diagnostic detail in logs and crash reports. Design the app-wide error model: how you separate domain errors from transport errors, how a lower-layer error is wrapped or mapped as it crosses each boundary, how the presentation layer turns any error into a user-facing message, and what you log versus what you show. Say where you would and would not use typed throws, and how you keep the model stable as new failure kinds appear.
You are designing the error model for a networked iOS app. It has a transport layer built on Apple's networking API URLSession plus decoding, a domain layer with business rules such as insufficient funds, and a presentation layer built with the declarative-UI framework SwiftUI. Requests fail for many reasons — no connectivity, a 500, a malformed payload, an expired token, a declined operation. Product wants every failure to reach the user as a clear, localized message with an optional retry, while engineers want full diagnostic detail in logs and crash reports. Design the app-wide error model: how you separate domain errors from transport errors, how a lower-layer error is wrapped or mapped as it crosses each boundary, how the presentation layer turns any error into a user-facing message, and what you log versus what you show. Say where you would and would not use typed throws, and how you keep the model stable as new failure kinds appear.
Split transport errors from domain errors. Each boundary wraps the lower error, keeping the cause. Presentation maps errors to a localized message and retry flag; logs keep the full chain, unseen. Use typed throws only for closed failure sets.
Common mistakes
- ✗Collapsing transport and domain errors into one flat enum the whole app throws
- ✗Showing a raw underlying error to the user instead of a mapped localized message
- ✗Using typed throws everywhere, even where a layer's failure set is open-ended
Follow-up questions
- →Where does wrapping an error lose context, and how do you preserve the cause?
- →Why does typed throws hurt a public API whose failure set may still grow?