Monday, August 29, 2016

Error Handling Compared

Matt Gallagher:

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 monadic flatMap over multiple Result generating functions but Swift avoids making abstract mathematical concepts like map and flatMap a required part of the core language and instead makes the code look as though it is a simple, linear sequence of actions.

Matt Gallagher:

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 the catch handling, the IO monad would have propagated all the way to the output of the main function.

There’s also an almost total lack of signalling. Unless you look for the bind operator, do notation or the catch, return or fail functions, it’s difficult to know where IO 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

Landon Fuller

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)

Landon Fuller

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.

Leave a Comment