Something I'd like to add to this discussion is that a possible alternative to enums and union types is protocols, which would discourage people from trying to exhaustively handle each error case, and instead nudge them towards handling the errors using some common functionality that the errors have. This can be made more powerful with "blanket" (protocol-to-protocol) conformances like extension SomeErrorProtocol: MyErrorProtocol
. For example, there can be a protocol for errors that can display a localized error message or graphical representation for a specific application, while existing types like CodingError
or DistributedActorSystemError
can be retroactively conformed to it as necessary.
I also wonder if it would be helpful to think of two "kinds" of error handling:
- General errors, which can potentially come from anywhere in the application. These are normally boxed in existentials and passed around in general-purpose error handling code. Callers should just let them propagate up the stack in most situations, since exhaustively handling each possible case would be impractical.
- Domain-specific errors, which warrant explicit handling of each possible case close to the call site. These don't necessary have to conform to
Error
, and not conforming toError
would prevent them from accidentally "escaping" into general-purpose error handling code if they would not be suitable for it.- Using non-
Error
-conforming enums for domain-specific errors would also prevent a certain class of bugs, where you accidentally handle errors that come from deeper in the call stack than you intended. This occasionally comes up in languages like Python, where exceptions likeKeyError
(from__getitem__
),StopIteration
(from__next__
), andValueError
(when converting strings to other types like int or Decimal) usually signal a special condition to be handled by the caller, but catching them can potentially mask errors deeper in the call stack.
- Using non-
Building on one of @wadetregaskis's examples:
func search(query: String) throws SearchError {
...
}
// Does *not* conform to `Error`, since `.emptyQuery`
// should be handled as close to the call site as possible,
// and `.other` needs to be unwrapped from the enum before
// being propagated to general-purpose error handling code.
enum SearchError {
case emptyQuery
case other(any Error)
}
do {
try search(query: "query")
} catch .emptyQuery {
// domain-specific error handling...
} catch .other(let error) {
throw error
}