Error Handling Compared
The
Result
type is so useful that it was almost included in the Swift standard library and even its rejection reveals an interesting look at the philosophies underpinning Swift’s design.[…]
Some implementations of
Result
use a generic parameter for the error […] Frankly, until Swift supports structural sum types (and there is no guarantee that it ever will), this can potentially involve a lot of manual work propagating errors to communicate a small amount of additional type information that the interface user will promptly ignore by treating all errors identically (bail out on any error).[…]
The effect of Swift’s error handling over successive
throws
statements is equivalent to the monadicflatMap
over multipleResult
generating functions but Swift avoids making abstract mathematical concepts likemap
andflatMap
a required part of the core language and instead makes the code look as though it is a simple, linear sequence of actions.
Given that all of the Swift compiler developers are themselves C++ developers, it is interesting that Swift has turned out almost, but not quite, entirely unlike C++. While Swift’s error handling offers potentially similar control flow to C++ exceptions, C++ exceptions provided the clearest example of what the Swift developers wanted to avoid with Swift.
Swift would also rather solve problems with clear syntax rather than the numerous safe implementation rules required in C++. The
defer
syntax used to manage cleanup at scope exit, including around thrown errors, is an example of language syntax avoiding the need for safe implementation rules like RAII.[…]
However, Haskell’s approach to error handling does have some limitations. Specifically, monadic handling makes it very easy to “bind” (
>>=
) to get the “success” result and totally ignore what happens in an error case. Monadic handling encourages the ignoring of errors. If this code had omitted thecatch
handling, theIO
monad would have propagated all the way to the output of themain
function.There’s also an almost total lack of signalling. Unless you look for the bind operator,
do
notation or thecatch
,return
orfail
functions, it’s difficult to know whereIO
or other monads are involved. Haskell’s pervasive type inferencing is often a hindrance here: only one of these functions is required to actually specify a type signature.[…]
While Swift has copied some of the syntactic elements of [Java] checked exceptions, Swift is very careful to avoid calling its errors “exceptions” since they are different in important ways. Most significantly, Swift’s errors are as performant as returning an error code and have no overlap with technology intended for fatal errors.
3 Comments RSS · Twitter
One other comment regarding API clients "treating all errors identically (bail out on any error)."
Not handling errors is the API client's prerogative, but when we're unable to define the failure case semantics of an APIs, then treating errors identically is the only thing callers *can* do.
Take a datastore query API; *I* certainly want the type system to help tell me if a particular call can return one of:
DataError:
- StoreUnreadable
- IOError (your file system is exploding)
- FormatUnsupported (we recognized the data store, but it's a later version we do not support)
- Locked (another process holds an exclusive lock)
- NotFound (everything worked fine, but the entry you requested wasn't found)
I'm struggling with "Monadic handling encourages the ignoring of errors", given that monadic error handling makes it impossible to ignore errors by definition.
What allows errors to be ignored is treating the 'error' case as an untyped value, as is the case in Swift.
Using a pseudo-syntax that might be more clear than Haskell, consider a Result[E, T] monadic disjoint union type, where `E` represents the error type, and `T` represents the success type.
If you try to pass a Result[SQLError, SQLRow] to an API that does not explicitly handle SQLError types, you'll get a type error; you can't just ignore the error.
This creates an error boundary; you *must* address the errors that may occur at that boundary, mapping them to a type that is expected and handled, rather than simply ignoring them and passing them on.
On the other hand, if what you have is simply a Result[SQLRow] with an untyped 'Error' attached, nothing stops you from passing it up to code that will truly have no idea what to do with it.