Part 1: result
, Lwt, and Lwt-result
#
This is Part 1 of 4 of the The Error Monad tutorial.
The result
type#
The type result
is part of the standard library of OCaml. It is
defined as
type ('a, 'b) result =
| Ok of 'a
| Error of 'b
(See reference manual.)
The constructors of the result
type have meaning: Ok
is for
normal, successful cases that carry a value that is somewhat expected.
Error
is for abnormal, failure cases that carry information about
what went wrong. E.g.,
let get a index =
if index < 0 then
Error "negative index in array access"
else if index >= Array.length a then
Error "index beyond length in array access"
else
Ok a.(index)
You can see the result
type as an opinionated either
: a
left-or-right sum type where the left and right side have distinct
roles. Or you can see the result
type as a buffed-up option
type: a type that either carries a value or doesn’t (but in this case it
carries an error instead).
Exercises#
Write the implementation for
(** [to_option r] is [Some x] if [r] is [Ok x]. Otherwise it is [None]. *) val to_option : ('a, unit) result -> 'a option
Write the implementation for
(** [to_option ?log r] is [Some x] if [r] is [Ok x]. Otherwise it calls [log e] and returns [None] if [r] is [Error e]. By default [log] is [ignore]. *) val to_option : ?log:('e -> unit) -> ('a, 'e) result -> 'a option
Write the implementation for
(** [catch f] is [Ok (f ())] except if [f] raises an exception [exc] in which case it is [Error exc]. *) val catch : (unit -> 'a) -> ('a, exn) result
The binding operator#
Working directly with the result
type can quickly become cumbersome.
Consider, for example the following code.
(** [compose3 f g h x] is [f (g (h x))] except that it handles errors. *)
let compose3 f g h x
match h x with
| Error e -> Error e
| Ok y ->
match g y with
| Error e -> Error e
| Ok z ->
f z
The nested match
-with
constructs lead to further and further
indentation. The Error
cases are all identical and simply add noise.
To circumvent this, in Octez we use a binding
operator: a user-defined
let
-binding. Specifically, you can open the Result_syntax
module
which includes the binding operator let*
dedicated to result
.
(** [compose3 f g h x] is [f (g (h x))] except that it handles errors. *)
let compose3 f g h x
let open Result_syntax in (* adds [let*] in scope *)
let* y = h x in
let* z = g y in
f z
An expression let* x = e in e'
is equivalent to
Result.bind e (fun x -> e')
and also to
match e with | Error err -> Error err | Ok x -> e'
. In all of these forms,
the expression e'
is evaluated if and only if the expression e
is
successful (i.e., evaluates to Ok
).
Exercises#
Rewrite the following code without
match
-with
let apply2 (f, g) (x, y) = match f x with | Error e -> Error e | Ok u -> match g y with | Error e -> Error e | Ok v -> Ok (u, v)
Did you remember to open the syntax module?
Write the implementation for
(** [map f [x1; x2; x3; ..]] is a list [[y1; y2; y3; ..]] where [y1 = f x1], [y2 = f x2], etc. except if [f] is fails on one of the inputs, in which case it is an [Error] carrying the same error as [f]'s. *) val map : ('a -> ('b, 'err) result) -> 'a list -> ('a list, 'err) result
Note that this exercise is for learning only. You won’t need to write this function in Octez. Indeed, a helper function which does exactly that is provided in the extended standard library of Octez.
Aside: the Error_monad
module is opened everywhere#
In Octez, the Error_monad
module provides types and values for error
management. It is part of the tezos-error-monad
package. And it is
opened in most of the source code. Apart from some specific libraries
(discussed later), the content of this module is already in scope.
The Error_monad
module contains the Result_syntax
module. This
is why you can use let open Result_syntax in
directly in your own
functions.
The Error_monad
module contains other modules and functions and
types which you will learn about later.
Recovering from errors#
When given a value of type result
, you can inspect its content to
determine if it is an Ok
or an Error
. You can use this feature
to recover from the failures.
match e with
| Ok x -> do_something x
| Error e -> do_something_else e
Recovering can mean different things depending on the task that failed and the error with which it failed. Sometimes you just want to retry, sometimes you want to retry with a different input, sometimes you want to propagate the error, sometimes you want to log the error and continue as if it hadn’t happened, etc.
let rec write data =
match write_to_disk data with
| Ok () -> ()
| Error EAGAIN -> write data (* again: try again *)
| Error ENOSPC -> Stdlib.exit 1 (* no space left on device: escalate to exit *)
There is a correspondence between a match
-with
around a
result
and a try
-with
. Both are for recovering from
failures. However, in Octez, you will mostly use a match
-with
around a result
, because we favour result
over exceptions. You
may use the try
-with
construct when interfacing with an external
library which uses exceptions.
There are several ways to use the match
-with
recovery with the
binding operator from Result_syntax
. Depending on the size of the
expression you are recovering from, one may be more readable than the
other. Choose accordingly.
You can simply place the expression directly inside the
match
-with
.
match
let* x = get_horizontal point in
let* y = get_vertical point in
Ok (x, y)
with
| Ok (x, y) -> ..
| Error e -> ..
Alternatively, if the expression grows too much in size or in
complexity, you can move the expression inside a vanilla
let
-binding: let r = .. in match r with ..
.
Alternatively, if the expression grows even more, or if the expression
may be re-used in other parts of the code, you may move the expression
inside a vanilla function which you can call inside the match
-with
.
You can also use the functions from the standard library’s Result module. Note however, that some of these functions are shadowed in the extended library of Octez, which you will learn more about later.
Mixing different kinds of errors#
Occasionally, you may have to call a function which returns a value of
type, say, (_, unit) result
and one, say, (_, string) result
. In
this case, you cannot simply bind the two expressions as is.
Specifically, the type checker will complain. You can see the constraint
you would be breaking in the type of let*
where the two error types
are the same ('e
):
val ( let* ) : ('a, 'e) result -> ('a -> ('b, 'e) result) -> ('b, 'e) result
When you need to mix those function, you have to either handle the
errors of each independently (see the section above about recovering from
errors) or you need to convert the errors so they have the same type.
You should use Result.map_error
to do that.
let* cfg =
Result.map_error (fun () -> "Error whilst reading configuration")
@@ read_configuration ()
in
..
The Result_syntax
module#
You have already learned about the let*
binding operator from the
Result_syntax
. But there are other values you can use from this
module.
The following functions form the core of the Result_syntax
module.
let*
: a binding operator to continue with the value in theOk
constructor or interrupt with the error in theError
constructor.let* x = e in e'
is equivalent tomatch e with Error err -> Error err | Ok x -> e'
.(See above for examples and more details.)
return : 'a -> ('a, 'e) result
: the expressionreturn x
is equivalent toOk x
. The function is included for consistency with other syntax modules presented later. You can use either form.fail : 'e -> ('a, 'e) result
: the expressionfail e
is equivalent toError e
. The function is included for consistency with other syntax modules presented later. You can use either form.
The following functions offer additional, less often used functionalities.
both : ('a, 'e) result -> ('b, 'e) result -> ('a * 'b, 'e list) result
: the expressionboth e1 e2
isOk
if both expressions evaluate toOk
andError
otherwise.Note that the expression
both e1 e2
is different from the expressionlet* x = e1 in let* y = e2 in return (x, y)
. In the former (both
) version, bothe1
ande2
are evaluated, regardless of success/failure ofe1
ande2
. In the latter (let*
-let*
) version,e2
is evaluated if and only ife1
is successful.This distinction is important when the expressions
e1
ande2
have side effects:both (f ()) (g ())
.all : ('a, 'e) result list -> ('a list, 'e list) result
: the functionall
is a generalisation ofboth
from tuples to lists.Note that, as a generalisation of
both
, in a call to the functionall
, all the elements of the list are evaluated, regardless of success/failure of the elements:all (List.map f xs)
.join : (unit, 'e) result list -> (unit, 'e list) result
: the functionjoin
is a specialisation ofall
for lists of unit-typed expressions (typically, for side-effects).Note that, as a specialisation of
all
, in a call to the functionjoin
, all the elements of the list are evaluated, regardless of success/failure of the elements:join (List.map f xs)
.
The following functions do not provide new functionalities but they are useful for small performance gains or for shorter syntax.
return_unit
is equivalent toreturn ()
but it avoids one allocation. This is important in parts of the code that are performance critical, and it is a good habit to take otherwise.return_nil
is equivalent toreturn []
but it avoids one allocation.return_true
is equivalent toreturn true
but it avoids one allocation.return_false
is equivalent toreturn false
but it avoids one allocation.return_none
is equivalent toreturn None
but it avoids one allocation.return_some x
is equivalent toreturn (Some x)
and it is provided for completeness withreturn_none
.let+
is a binding operator similar tolet*
but when the expression which follows thein
returns a non-result value. In other words,let+ x = e in e'
is equivalent tolet* x = e in return (e')
.The
let+
is purely for syntactic conciseness (compared to the*
variant), use it if it makes your code more readable.
Lwt#
In Octez, I/O and concurrency are handled using the Lwt library. With Lwt you use promises to handle I/O and concurrency. You can think of promises as data structures that are empty until they resolve, at which point they hold a value.
A promise for a value of type 'a
has type 'a Lwt.t
. The function
Lwt.bind : 'a t -> ('a -> 'b t) -> 'b t
waits for the promise to
resolve (i.e., to carry a value of type 'a
) before applying the
provided function. The expression bind p f
is a promise which
resolves only once the promise p
has resolved and then the promise
returned by f
has resolved too.
If you are not familiar with Lwt, you should check out the official manual and this introduction before continuing. This is important. Do it.
Unlike is mentioned in those separate resources on Lwt, in Octez, we
do not in general use the success/failure mechanism of Lwt. Instead, we
mostly rely on result
(as mentioned above).
Thus, in the rest of this tutorial we only consider the subset of Lwt without failures. In practice, you might need to take care of exceptions in some cases, but this is discussed in the later, more advanced parts of the tutorial.
The Lwt_syntax
module#
In Octez, because Lwt is pervasive, you need to bind promises often. To
make it easier, you can use the Lwt_syntax
module. The
Lwt_syntax
module is made available everywhere by the
Error_monad
module. The Lwt_syntax
module is similar to the
Result_syntax
module but for the Lwt monad (more about monads
later).
let*
: a binding operator to wait for the promise to resolve before continuing.let* x = e in e'
is a promise that resolves aftere
has resolved to a given value and thene'
has resolved with that value carried byx
.Note that
Lwt_syntax
andResult_syntax
(see above) both uselet*
for their main binding operator. Consequently, the specific meaning oflet*
depends on which module is open. This extends to other syntax modules introduced later in this tutorial.(What if you need to use both Lwt and
result
? Which syntax module should you use? You will learn about that in the next section!)return : 'a -> 'a Lwt.t
: the expressionreturn x
is equivalent toLwt.return x
. It is a promise that is already resolved with the value ofx
.and*
: a binding operator alias forboth
(see below). You can use it withlet*
the same way you useand
withlet
.let apply_triple f (x, y, z) = let open Lwt_syntax in let* u = f x and* v = f y and* w = f z in return (u, v, w)
When you use
and*
, the bound promises (f x
,f y
, andf z
) are evaluated concurrently, and the expression which follows thein
(return ..
) is evaluated once all the bound promises have all resolved.
The following functions offer additional, less often used functionalities.
both: 'a Lwt.t -> 'b Lwt.t -> ('a * 'b) Lwt.t
: the expressionboth p q
is a promise that resolves only once both promisesp
andq
(which make progress concurrently) have resolved.In practice, you will most likely use
and*
instead of both.all: 'a Lwt.t list -> 'a list Lwt.t
: the functionall
is a generalisation ofboth
from tuples to lists.Note that, as a generalisation of
both
, in a call to the functionall
, all the promises in the provided list make progress towards resolution concurrently.join : unit Lwt.t list -> unit Lwt.t
: the functionjoin
is a specialisation ofall
to lists of units (i.e., side-effects).
The following functions do not provide new functionalities but they are useful for small performance gains or for shorter syntax.
return_unit
is equivalent toreturn ()
but it avoids one allocation. This is important in parts of the code that are performance critical, and it is a good habit to take otherwise.return_nil
is equivalent toreturn []
but it avoids one allocation.return_true
is equivalent toreturn true
but it avoids one allocation.return_false
is equivalent toreturn false
but it avoids one allocation.return_none
is equivalent toreturn None
but it avoids one allocation.return_some x
is equivalent toreturn (Some x)
and it is provided for completeness.return_ok x
is equivalent toreturn (Ok x)
and it is provided for completeness.return_error x
is equivalent toreturn (Error x)
and it is provided for completeness.let+
andand+
are binding operators similar tolet*
andand*
but when the expression which follows thein
returns a non-promise value. In other words,let+ x = e1 and+ y = e2 in e
is equivalent tolet* x = e1 and* y = e2 in return e
.The
let+
andand+
are purely for syntactic conciseness (compared to the*
variants), use them if it makes your code more readable.
Promises of results: Lwt and result
together#
In Octez, we have functions that perform I/O and also may fail. In this
case, the function returns a promise of a result
. This is the topic
of this section.
Note that Lwt and result
are orthogonal concerns. On the one hand,
Lwt is for concurrency, for automatically scheduling code around I/O,
for making progress on different parts of the program side-by-side. On
the other hand, result
is for aborting computations, for handling
success/failures. It is because Lwt and result
are orthogonal that
we can use them together.
'a --------------> ('a, 'e) result
| |
| |
V V
'a Lwt.t ---------> ('a, 'e) result Lwt.t
When we combine Lwt and result
for control-flow purpose we combine
both of the orthogonal behaviours. We can achieve this combined
behaviour “by hand”. However, doing so requires mixing
Lwt_syntax.( let* )
and regular match
-with
:
let apply2 (f, g) (x, y) =
let open Lwt_syntax in
let* r = f x in
match r with
| Error e -> return (Error e)
| Ok u ->
let* r = g y in
match r with
| Error e -> return (Error e)
| Ok v -> return (Ok (u, v))
This is interesting to consider because it shows the two orthogonal
features of control-flow separately: wait for the promise to resolve,
and check for errors. However, in practice, this becomes cumbersome even
faster than when working with plain result
values.
To make this easier, in Octez we use a binding operator. Specifically,
you can open the Lwt_result_syntax
(instead of the other syntax
modules) which includes a binding operator dedicated to promises of
result
.
let apply2 (f, g) (x, y) =
let open Lwt_result_syntax in
let* u = f x in
let* v = g y in
return (u, v)
When a promise resolves to Ok
we say that it resolves successfully.
When it resolves to Error
we say that it resolves unsuccessfully or
that it fails.
Exercises#
Rewrite the following code without
match
-with
let compose3 f g h x let open Lwt_syntax in let* r = h x in match r with | Error e -> return (Error e) | Ok y -> let* s = g y in match s with | Error e -> return (Error e) | Ok z -> f z
Did you remember to change the opened syntax module?
The Lwt_result_syntax
module#
Octez provides the Lwt_result_syntax
module to help handle promises
of results.
let*
: a binding operator to wait for the promise to resolve before continuing with the value in theOk
constructor or interrupting with the error in theError
constructor.Note how the
let*
binding operator combines the behaviour ofLwt_syntax.( let* )
andResult_syntax.( let* )
. Also note that the differentlet*
s are differentiated by context; specifically by what syntax module has been opened.return: 'a -> ('a, 'e) result Lwt.t
: the expressionreturn x
is a promise already successfully resolved tox
. More formally,return x
is equivalent toLwt_syntax.return (Result_syntax.return x)
.fail: 'e -> ('a, 'e) result Lwt.t
: the expressionfail e
is a promise already unsuccessfully resolved with the errore
. More formally,fail e
is equivalent toLwt_syntax.return (Result_syntax.fail e)
.
The following functions offer additional, less often used functionalities.
both : ('a, 'e) result Lwt.t -> ('b, 'e) result Lwt.t -> ('a * 'b, 'e list) result Lwt.t
: the expressionboth p1 p2
is a promise that resolves successfully if bothp1
andp2
resolve successfully. It resolves unsuccessfully if eitherp1
orp2
resolve unsuccessfully.Note that in the expression
both p1 p2
, both promisesp1
andp2
are evaluated concurrently. Moreover, the returned promise only resolves once both promises have resolved, even if one resolves unsuccessfully.Note that this syntax module does not offer
and*
as a binding operator alias forboth
. This is because, as withResult_syntax
, the type for errors inboth
is not stable (it is'e
on the argument side and'e list
on the return side). This hinders practical uses ofand*
.all : ('a, 'e) result Lwt.t list -> ('a list, 'e list) result Lwt.t
: the functionall
is a generalisation ofboth
from tuples to lists.Note that, as a generalisation of
both
, in a call to the functionall
, all the promises in the provided list make progress towards resolution concurrently and continue to evaluate until resolution regardless of successes and failures.join : (unit, 'e) result Lwt.t list -> (unit, 'e list) result Lwt.t
: the functionjoin
is a specialisation ofall
for lists of unit-type expressions (typically, for side-effects).
The following functions do not provide new functionalities but they are useful for small performance gains or for shorter syntax.
return_unit
is equivalent toreturn ()
but it avoids one allocation. This is important in parts of the code that are performance critical, and it is a good habit to take otherwise.return_nil
is equivalent toreturn []
but it avoids one allocation.return_true
is equivalent toreturn true
but it avoids one allocation.return_false
is equivalent toreturn false
but it avoids one allocation.return_none
is equivalent toreturn None
but it avoids one allocation.return_some x
is equivalent toreturn (Some x)
and it is provided for completeness.Note that, like with
Result_syntax
, this syntax module does not providereturn_ok
andreturn_error
. This is to avoid nestedresult
types. If you need to nestresult
s you can do so by hand.let+
is a binding operator similar tolet*
but when the expression which follows thein
returns a non-promise value. In other words,let+ x = e in e'
is equivalent tolet* x = e in return (e')
.The
let+
is purely for syntactic conciseness (compared to the*
variant), use it if it makes your code more readable.
Exercises#
Write the implementation for
(** [map f [x1; x2; ..]] is a promise for a list [[y1; y2; .. ]] where [y1] is the value that [f x1] successfully resolves to, etc. except if [f] resolves unsuccessfully on one of the input in which case it also resolves unsuccessfully with the same error as [f]. *) map : ('a -> ('b, 'e) result Lwt.t) -> 'a list -> ('b list, 'e) result Lwt.t
How does your code compare to the one in the
result
-only variant of this exercise?Note that this exercise is for learning only. You won’t need to write this function in Octez. Indeed, a helper function which does exactly that is provided in the extended standard library of Octez.
Make your
map
function tail-recursive.What type error is triggered by the following code?
open Lwt_result_syntax ;; let ( and* ) = both ;; let _ = let* x = return 0 and* y = return 1 in let* z = return 2 in return (x + y + z) ;;
Rewrite the following function without
match
-with
let compose3 f g h x = let open Lwt_syntax in let* y_result = f x in match y_result with | Error e -> return (Error e) | Ok y -> let* z_result = g y in match z_result with | Error e -> return (Error e) | Ok z -> h z
Converting errors of promises#
Remember that, with Result_syntax
, you cannot mix different types of
errors in a single sequence of let*
. This also applies to
Lwt_result_syntax
. Indeed, the type checker will prevent you from
doing so.
You can use the same Result.map_error
function as for plain
result
s. But when you are working with promises of result
, the
syntactic cost of doing so is high:
let open Lwt_result_syntax in
let* config =
let open Lwt_syntax in
let* config_result = read_configuration () in
Lwt.return (Result.map_error (fun () -> ..) config_result)
in
..
To avoid this syntactic weight, the Lwt_result_module
provides a
dedicated function:
lwt_map_error : ('e -> 'f) -> ('a, 'e) result Lwt.t -> ('a, 'f) result Lwt.t
Lifting#
Occasionally, whilst you are working with promises of result
(i.e.,
working with values of the type (_, _) result Lwt.t
), you will need
to call a function that returns a simple promise (a promise that cannot
fail, a promise of a value that’s not a result
, i.e., a value of
type _ Lwt.t
) or a simple result (an immediate value of a
result
, i.e., a value of type (_, _) result
). This is common
enough that the module Lwt_result_syntax
provides helpers dedicated
to this.
From result
-only into Lwt-result
#
The module Lwt_result_syntax
includes the binding operator let*?
. It is
dedicated to binding Result-only expressions.
let*? x = check foo bar in (* Result-only: checking doesn't yield *)
..
From Lwt-only into Lwt-result
#
The module Lwt_result_syntax
includes the binding operator let*!
. It is
dedicated to binding Lwt-only expressions.
let*! x = Events.emit foo bar in (* Lwt-only: logs can't fail *)
..
Wait! There is too much! What module am I supposed to open locally and what operators should I use?#
If you are feeling overwhelmed by the different syntax modules, here are some simple guidelines.
If your function returns
(_, _) result Lwt.t
values, then you start the function withlet open Lwt_result_syntax in
. Within the function you uselet
for vanilla expressions,let*
for Lwt-result
expressions,let*!
for Lwt-only expressions,let*?
forresult
-only expressions.
And you end your function with a call to
return
.If your function returns
(_, _) result
values, then you start the function withlet open Result_syntax in
. Within the function you uselet
for vanilla expressions,let*
forresult
expressions,
And you end your function with a call to
return
.If your function returns
_ Lwt.t
values, then you start the function withlet open Lwt_syntax in
.let
for vanilla expressions,let*
for Lwt expressions,
And you end your function with a call to
return
.
These are rules of thumb and there are exceptions to them. Still, they should cover most of your uses and, as such, they are a good starting point.
What’s an error?#
So far, this tutorial has considered errors in an abstract way. Most of
the types carried by the Error
constructors have been parameters
('e
). This is a common pattern for higher-order functions that
compose multiple result
and Lwt-result
functions together. But,
in practice, not every function is a higher-order abstract combinator
and you sometimes need to choose a concrete error. This section explores
common choices.
A dedicated algebraic data type
Often, a dedicated algebraic data type is appropriate. A sum type
represents the different kinds of failures that might occur. E.g.,
type hashing_error = Not_enough_data | Invalid_escape_sequence
. A
product type (typically a record) carries multiple bits of information
about a failure. E.g.,
type parsing_error = { source: string; location: int; token: token; }
This approach works best when a set of functions (say, all the functions of a module) have similar ways to fail. Indeed, when that is the case, you can simply define the error type once and all calls to these functions can match on that error type if need be.
E.g., binary encoding and decoding errors in data-encoding.
Polymorphic variants
In some cases, the different functions of a module may each fail with different subsets of a common set of errors. In such a case, you can use polymorphic variants to represent errors. E.g.,
val connect_to_peer:
address -> (connection, [ `timeout | `connection_refused ]) result Lwt.t
val send_message:
connection -> signing_key -> string ->
(unit, [ `timeout | `connection_closed ]) result Lwt.t
val read_message:
connection ->
(string, [ `timeout | `unknown_signing_key | `invalid_signature | `connection_closed ]) result Lwt.t
val close_connection: connection -> (unit, [ `unread_messages of string list ]) result
The benefit of this approach is that the caller can compose the different functions together easily and match only on the union of errors that may actually happen. The type checker keeps track of the variants that can reach any program point.
let handshake conn =
let open Lwt_result_syntax in
let* () = send_message conn "ping" in
let* m = read_message conn in
if m = "pong" then
return ()
else
`unrecognised_message m
let handshake conn =
let open Lwt_syntax in
let* r = handshake conn in
match r with
| Ok () -> return_unit
| Error (`unknown_signing_key | `invalid_signature) ->
(* we ignore unread messages if the peer had signature issues *)
let _ = close_connection conn in
return_unit
| Error (`timeout | `connection_closed) ->
match close_connection with
| Ok () -> return_unit
| Error (`unread_messages msgs) ->
let* () = log_unread_messages msgs in
return_unit
A human-readable string
In some cases, there is nothing to be done about an error but to inform the user. In this case, the error may just as well be the message.
It is important to note that these messages are not generally meant to be matched against. Indeed, such messages may not be stable and even if they are, they probably don’t carry precise enough information to be acted upon.
You should only use string
as an error type when the error is not
recoverable and you should not try to recover from string
errors (or
more precisely, your recovery should not depend on the content of the
string).
An abstract type
If the error is not meant to be recovered from, it is sometimes ok to use an abstract type. This is generally useful at the interface of a module, specifically when the functions within the module are meant to inspect the errors and possibly attempt recovery, but the callers outside of the modules are not.
If you do use an abstract type for errors, you should also provide a pretty-printing function.
A wrapper around one of the above
Sometimes you want to add context or information to an error. E.g.,
type 'a with_debug_info = {
payload: 'a;
timestamp: Time.System.t;
position: string * int * int * int;
}
let with_debug_info ~position f =
match f () with
| Ok _ as ok -> ok
| Error e -> Error { payload = e; timestamp = Time.System.now (); position }
This specific example can be useful for debugging, but other wrappers can be useful in other contexts.
Mixing error types
It is difficult to work with different types of errors within the same function. This most commonly happens if you are calling functions from different libraries, which use different types of errors.
This is difficult because the errors on both sides of the binding operator are the same.
val ( let* ) : ('a, 'e) result -> ('a -> ('b, 'e) result) -> ('b, 'e) result
The error monad provides some support to deal with multiple types of errors at once. But this support is limited. It is not generally an issue because error-mixing is somewhat rare: it tends to happen at the boundary between different levels of abstractions.
If you encounter one of these situations, you will need to convert all the errors to a common type.
type error = Configuration_error | Command_line_error
let* config =
match Config.parse_file config_file with
| Ok _ as ok -> ok
| Error Config.File_not_found -> Ok Config.default
| Error Config.Invalid_file -> Error Configuration_error
in
let* cli_parameters =
match Cli.parse_parameters () with
| Ok _ as ok -> ok
| Error Cli.Invalid_parameter -> Error Command_line_error
in
..
You can also use the Result.map_error
and lwt_map_error
functions introduced in previous sections.
Wait! It was supposed to be “one single uniform way of dealing with errors”! What is this?#
The error management in Octez is a unified way (syntax modules with regular, predictable interfaces) of handling different types of errors.
The variety of errors is a boon in that it lets you use whatever is the most appropriate for the part of the code that you are working on. However, the variety of errors is also a curse in that stitching together functions which return different errors requires boilerplate conversion functions.
That’s where the global error
type comes in: a unified type for
errors. And that’s for the next section to introduce.
META COMMENTARY#
The previous sections are not Octez-specific. True, the syntax modules are defined within the Octez source tree, but they could be released separately (and they will be) or they could easily be replicated in a separate project.
The next sections are Octez-specific. They introduce types and values that are used within the whole of Octez.