We hope you've familiarized yourself with some of the common ways tohandle errors with Unison data types,this doc will discuss a different strategy for error management, using abilities. A more comprehensive introduction to abilitiescan be found here,but this doc does not rely on a detailed understanding of algebraic effects or writing ability handlers. In this doc we'll go through some common patterns and examples for expressing and handling errors with abilities in Unison.
Why use abilities for error handling?
Abilities allow us to express the possibility that an error might occur in a programwithoutthe additional complexity of introducing a data type to enclose a function's desired output. Errors are still captured in the type signatures of functions throughability requirements,so you're still able to reason about the behavior of your program at the type level, but rather than managing the effect's presence in the remainder of the program through calls to functions likeflatMap
ormap
or special notation likedo
orfor expressions
,you can focus on the core behavior of your program, and delegate handling the error to enclosing handlers when absolutely necessary.
Abort
Abort
is an ability which expresses the termination of a program without additional information about the failure.
Abort
'ssolerequest constructorisabort
.Here's an example of when you might useabort
in a function:
Note that theAbort
ability requirementshows up in the return type of the function. This means that the caller of this function will either need to handle the ability or express the ability requirement in its own signature. In the example belowmyFunction
callserrorHandling.divBy
and therefore theAbort
ability requirement is present even thoughmyFunction
itself doesn't callabort
.
TheAbort
ability requirement can be eliminated by translating it to anOptional
value with the handlertoOptional!
.Function executions which encounter theabort
call get translated toOptional.None
and function calls that are successful are wrapped inSome
.
toOptional! '(errorHandling.divBy 4 2)⧨Some 2
Vice-versa, you can translate anOptional
value into theAbort
ability with theOptional.toAbort
function.
Some 4 |> toAbort
Throw
TheThrow
ability will stop a computation and return a value of aspecific typewhen an error condition occurs. For this reasonThrow
is parameterized by the possible error type in function signatures which require it.Throw
has one request constructor,throw
.Rewriting theerrorHandling.divBy
function to useThrow
yields:
We've chosen tothrow
a value of typeText
here, but in a larger application you might encode the particulars of your error in a custom data type.
Structurally, theThrow
ability shares much in common with theEither
data type. Both capture information about the type of the error and both return a value of that type in the event of a failure. They offer the capability of providing enriched error reporting. You can eliminate the ability requirement forThrow
by translating it to aEither
with theThrow.toEither
handler.
Throw.toEither '(divByThrow 1 0)⧨Either.Left "Cannot divide by zero"
Exception
TheException
ability is similar to theThrow
ability, except the error type is pinned to be a data type calledFailure
.For more information about how to construct a value of typeFailure
check out this document here.When a failure occurs, the Exception ability's request constructor,Exception.raise
,surfaces relevantFailure
information to the enclosing handler. Many of the functions in thebase
library express the possibility of errors in terms of this ability.
Here's how we might rewrite our function usingException
:
divByException : Nat -> Nat ->{Exception} Nat
divByException : Nat -> Nat ->{Exception} Nat
divByException a b = match b with
0 -> Exception.raise (Generic.failure "Cannot divide by zero" b)
n -> a Nat./ b
In the above code, we're using theGeneric.failure
function to help generate aFailure
value. In a larger application with errors modeled as data types we might choose to construct our ownFailure
.
The base library provides a handler to translate theException
ability into value of typeEither Failure a
.
catch '(divByException 1 0)⧨Either.Left
(Failure (typeLink Generic) "Cannot divide by zero" (Any 0))
❗️ But if you're feeling bold you can also run…
unsafeRun! '(divByException 4 2)⧨2
If an exception is thrown the program will just crash.
Which strategy for error handling should I prefer?
Given the correspondence between Unison abilities and Unison data types and the ease with which you can translate between the two with functions provided by thebase
library--which should you reach for idiomatically?