I really like this post from Matt Parsons, The Trouble with Typed Errors. It’s written for an audience writing Haskell, but if you can grok Haskell syntax, it’s worth the read because the lessons apply broadly to most statically typed programming languages.
If you haven’t read it (or it’s been a while) the setup is basically: typing errors is hard, and nearly every solution is either brittle, clunky, verbose, or uses powerful type system features that we didn’t want to have to reach for.
Hidden towards the bottom of the post, we find:
In PureScript or OCaml, you can use open variant types to do this flawlessly. Haskell doesn’t have open variants, and the attempts to mock them end up quite clumsy to use in practice.
What Matt calls “open variant types” I call ad hoc union types (see my previous post about checked exceptions and Sorbet). Naming aside, Sorbet has them! We don’t have to suffer from clunky error handling!
I thought it’d be interesting to show what Matt meant in this quote by translating his example to Sorbet.
I wrote a complete, working example, but rather than repeat the whole thing here, I’m just going to excerpt the good stuff. If you’re wondering how something is defined in full, check the full example:
First, here’s how we’d type the three running helper methods from Matt’s post:
Notice how in all three cases, we use a normal Sorbet union type in
the return, like
T.any(String, HeadError). All of the error types are
just user-defined classes. For example,
HeadError is just defined like
Then at the caller side, it’s simple to handle the errors:
The idea is that the return type includes the possible errors, so we
have to handle them. This example handles the errors by checking for
success and returning early with the error otherwise. This manifests in
the return type of
foo, which mentions four outcomes:
- a successful result (
- three kinds of failures (
It would have worked equally well to handle and recover from any or all of the errors: Sorbet knows exactly which error is returned by which method, so there’s never a burden of handling more errors than are possible.
It’s fun that what makes this work is Sorbet’s natural flow-sensitive
typing, not some special language feature. Notice how before and after
the first early return, Sorbet updates its knowledge of the type of
(shown in the comments) because it knows how
Another example: if some other method only calls
head), it doesn’t have to mention
HeadError in its return:
And while there’s never a need to predeclare one monolithic error type
AllErrorsEver in Matt’s post), if it happens to be convenient,
Sorbet still lets you, using type aliases. For example, maybe there are
a bunch of methods that all return
can factor that out into a type alias:
That’s it! Sorbet’s union types in method returns provide a low-friction, high value way to model how methods can fail.