We hope you've familiarized yourself with some of the common ways to handle errors with Unison data types, this doc will discuss a different strategy for error management, using abilities. A more comprehensive introduction to abilities can 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 program without the additional complexity of introducing a data type to enclose a function's desired output. Errors are still captured in the type signatures of functions through ability 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 like flatMap or map or special notation like do or for 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's sole request constructor is Abort.abort. Here's an example of when you might use Abort.abort in a function:
Note that the Abort ability requirement shows 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 below myFunction calls errorHandling.divBy and therefore the Abort ability requirement is present even though myFunction itself doesn't call Abort.abort.
The Abort ability requirement can be eliminated by translating it to an Optional value with the handler toOptional!. Function executions which encounter the Abort.abort call get translated to Optional.None and function calls that are successful are wrapped in Some.
toOptional! do errorHandling.divBy 4 2⧨Some 2Vice-versa, you can translate an Optional value into the Abort ability with the Optional.toAbort function.
Some 4 |> toAbortThrow
The Throw ability will stop a computation and return a value of a specific type when an error condition occurs. For this reason Throw is parameterized by the possible error type in function signatures which require it. Throw has one request constructor, throw. Rewriting the errorHandling.divBy function to use Throw yields:
We've chosen to throw a value of type Text here, but in a larger application you might encode the particulars of your error in a custom data type.
Structurally, the Throw ability shares much in common with the Either 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 for Throw by translating it to a Either with the toEither handler.
toEither do divByThrow 1 0⧨Either.Left "Cannot divide by zero"Exception
The Exception ability is similar to the Throw ability, except the error type is pinned to be a data type called Failure. For more information about how to construct a value of type Failure check out this document here. When a failure occurs, the Exception ability's request constructor, Exception.raise, surfaces relevant Failure information to the enclosing handler. Many of the functions in the base library express the possibility of errors in terms of this ability.
Here's how we might rewrite our function using Exception:
divByException : Nat -> Nat ->{Exception} NatdivByException : Nat -> Nat ->{Exception} Nat
divByException a b = match b with
0 -> Exception.raise (Generic.failure "Cannot divide by zero" b)
n -> a Nat./ bIn the above code, we're using the Generic.failure function to help generate a Failure value. In a larger application with errors modeled as data types we might choose to construct our own Failure.
The base library provides a handler to translate the Exception ability into value of type Either Failure a.
catch do 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! do divByException 4 2⧨2If 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 the base library—which should you reach for idiomatically?