throws vs Result vs optional — when do you use each?
Mental model. throws is a fire alarm. Result is a fire report you can file. An optional is “nobody was home”.
throws — typed failure at a function boundary. The caller must try, and the failure is handled at the point of the call. Best for user-initiated work where you can react immediately.
Result — a success/failure value you can store, pass, or deliver asynchronously (completion handlers, cached outcomes, Combine). It makes failure explicit in the type without using control flow.
Optional — absence is not an error. firstIndex(of:) returning nil means “not there”, not “something went wrong”. Do not wrap every miss in an Error.
| throws | Result | Optional | |
|---|---|---|---|
| Says why it failed | yes | yes | no |
| Call site | try | switch / get() | if let / ?? |
| Typed error | with throws(E) | yes | no |
| Store or pass around | no | yes | yes |
| Best for | most APIs | stored outcomes | absence is normal |
Common traps
- Using
Resulteverywhere because it is “more functional”. It adds unwrapping noise to synchronous code that could just throw. - Throwing an error for “not found”. If absence is normal, an optional is the honest type.
- Converting
throwstoResultinside the callee. Let callers choose;Result(catching:)exists for that.
They might follow up with
- What does
Result(catching:)do? Runs a throwing closure and packages the outcome as.successor.failure. - What about async?
async throwsis the default;Resultis still useful for stored or streamed outcomes.