Article

Error Handling Patterns in Swift

Bitesize

Swift has a few main ways to represent failure:

  • throws
  • Result
  • optionals
func loadUser() throws -> User {
    throw UserError.notFound
}

In most synchronous APIs, throws is the default choice.

More detail

An overview of Swift error handling styles including throws, Result, and optionals

The key is to match the error mechanism to the job:

  • use throws when the caller should handle a failure path directly
  • use Result when you need to store or pass success/failure as a value
  • use an optional only when the reason for absence does not matter

The mistake to avoid is mixing styles randomly across the same API surface. That forces callers to keep switching mental models.

Consistency matters more than people often expect. When one part of an API throws, another returns optionals, and a third uses Result, every call site has to re-learn how failure is modeled. That slows readers down and makes failure handling feel incidental.

A good API makes the error story predictable. The caller should quickly understand whether a failure can be inspected, propagated, retried, or safely ignored.

That is why error handling is not separate from interface design. It is part of how the caller experiences the whole API.

Deep dive

Error design is really API design. The more meaning your errors carry, the easier it is to build clear recovery behavior.

That is why typed domain errors are useful:

enum UserError: Error {
    case notFound
    case unauthorized
}

This gives you stronger semantics than a vague generic failure. Over time, consistent error modeling makes codebases easier to reason about because failure stops being an afterthought and becomes part of the contract.

This also affects logging and observability. If failures are modeled clearly, it becomes easier to distinguish between expected domain problems and genuinely exceptional conditions. That leads to cleaner debugging and better telemetry.

Another subtle benefit is test quality. Well-structured errors make it easier to assert on exact failure modes rather than vaguely checking that “something failed.” That tends to produce more meaningful tests and clearer intent.

There is also a difference between technical errors and domain errors. Network timeouts, decoding failures, and file access issues are not the same as a missing account or an expired subscription. Good error design respects that distinction instead of flattening everything together.

Over time, that separation helps teams build better recovery paths. Some failures should retry, some should surface to the user, and some should map into business-state decisions. If the error model is muddy, that logic becomes muddy too.

So the goal is not just to pick throws or Result. The goal is to make failure feel like a first-class part of the API contract, with enough structure that callers know what kind of response makes sense.

Finished the deep dive?

You made it to the end.

Mark this article as read once you have worked through the full piece. It is a small way to keep track of what you have genuinely finished.

More in this area

Keep the thread going.

Jump sideways into the related ideas that sit closest to this piece and keep the same mental context alive.

  1. 01 Error handling Explore topic