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 the Ok constructor or interrupt with the error in the Error constructor.

    let* x = e in e' is equivalent to match e with Error err -> Error err | Ok x -> e'.

    (See above for examples and more details.)

  • return : 'a -> ('a, 'e) result: the expression return x is equivalent to Ok x. The function is included for consistency with other syntax modules presented later. You can use either form.

  • fail : 'e -> ('a, 'e) result: the expression fail e is equivalent to Error 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 expression both e1 e2 is Ok if both expressions evaluate to Ok and Error otherwise.

    Note that the expression both e1 e2 is different from the expression let* x = e1 in let* y = e2 in return (x, y). In the former (both) version, both e1 and e2 are evaluated, regardless of success/failure of e1 and e2. In the latter (let*-let*) version, e2 is evaluated if and only if e1 is successful.

    This distinction is important when the expressions e1 and e2 have side effects: both (f ()) (g ()).

  • all : ('a, 'e) result list -> ('a list, 'e list) result: the function all is a generalisation of both from tuples to lists.

    Note that, as a generalisation of both, in a call to the function all, 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 function join is a specialisation of all for lists of unit-typed expressions (typically, for side-effects).

    Note that, as a specialisation of all, in a call to the function join, 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 to return () 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 to return [] but it avoids one allocation.
    return_true is equivalent to return true but it avoids one allocation.
    return_false is equivalent to return false but it avoids one allocation.
    return_none is equivalent to return None but it avoids one allocation.
    return_some x is equivalent to return (Some x) and it is provided for completeness with return_none.
  • let+ is a binding operator similar to let* but when the expression which follows the in returns a non-result value. In other words, let+ x = e in e' is equivalent to let* 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 after e has resolved to a given value and then e' has resolved with that value carried by x.

    Note that Lwt_syntax and Result_syntax (see above) both use let* for their main binding operator. Consequently, the specific meaning of let* 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 expression return x is equivalent to Lwt.return x. It is a promise that is already resolved with the value of x.

  • and*: a binding operator alias for both (see below). You can use it with let* the same way you use and with let.

    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, and f z) are evaluated concurrently, and the expression which follows the in (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 expression both p q is a promise that resolves only once both promises p and q (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 function all is a generalisation of both from tuples to lists.

    Note that, as a generalisation of both, in a call to the function all, all the promises in the provided list make progress towards resolution concurrently.

  • join : unit Lwt.t list -> unit Lwt.t: the function join is a specialisation of all 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 to return () 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 to return [] but it avoids one allocation.
    return_true is equivalent to return true but it avoids one allocation.
    return_false is equivalent to return false but it avoids one allocation.
    return_none is equivalent to return None but it avoids one allocation.
    return_some x is equivalent to return (Some x) and it is provided for completeness.
    return_ok x is equivalent to return (Ok x) and it is provided for completeness.
    return_error x is equivalent to return (Error x) and it is provided for completeness.
  • let+ and and+ are binding operators similar to let* and and* but when the expression which follows the in returns a non-promise value. In other words, let+ x = e1 and+ y = e2 in e is equivalent to let* x = e1 and* y = e2 in return e.

    The let+ and and+ 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 the Ok constructor or interrupting with the error in the Error constructor.

    Note how the let* binding operator combines the behaviour of Lwt_syntax.( let* ) and Result_syntax.( let* ). Also note that the different let*s are differentiated by context; specifically by what syntax module has been opened.

  • return: 'a -> ('a, 'e) result Lwt.t: the expression return x is a promise already successfully resolved to x. More formally, return x is equivalent to Lwt_syntax.return (Result_syntax.return x).

  • fail: 'e -> ('a, 'e) result Lwt.t: the expression fail e is a promise already unsuccessfully resolved with the error e. More formally, fail e is equivalent to Lwt_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 expression both p1 p2 is a promise that resolves successfully if both p1 and p2 resolve successfully. It resolves unsuccessfully if either p1 or p2 resolve unsuccessfully.

    Note that in the expression both p1 p2, both promises p1 and p2 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 for both. This is because, as with Result_syntax, the type for errors in both is not stable (it is 'e on the argument side and 'e   list on the return side). This hinders practical uses of and*.

  • all : ('a, 'e) result Lwt.t list -> ('a list, 'e list) result Lwt.t: the function all is a generalisation of both from tuples to lists.

    Note that, as a generalisation of both, in a call to the function all, 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 function join is a specialisation of all 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 to return () 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 to return [] but it avoids one allocation.
    return_true is equivalent to return true but it avoids one allocation.
    return_false is equivalent to return false but it avoids one allocation.
    return_none is equivalent to return None but it avoids one allocation.
    return_some x is equivalent to return (Some x) and it is provided for completeness.

    Note that, like with Result_syntax, this syntax module does not provide return_ok and return_error. This is to avoid nested result types. If you need to nest results you can do so by hand.

  • let+ is a binding operator similar to let* but when the expression which follows the in returns a non-promise value. In other words, let+ x = e in e' is equivalent to let* 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 results. 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 with let open Lwt_result_syntax in. Within the function you use

    • let for vanilla expressions,

    • let* for Lwt-result expressions,

    • let*! for Lwt-only expressions,

    • let*? for result-only expressions.

    And you end your function with a call to return.

  • If your function returns (_, _) result values, then you start the function with let open Result_syntax in. Within the function you use

    • let for vanilla expressions,

    • let* for result expressions,

    And you end your function with a call to return.

  • If your function returns _ Lwt.t values, then you start the function with let 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.

You should take this opportunity to take a break.
Come back in a few minutes.