Monadic Error Handling

Explaining what a monad is has become a classic exercise. One can find hundreds of articles about the topic and this is my take. It is low on theory and focuses on motivation and mechanics. I’m using OCaml for my code examples.

The Problem: Error Handling

Exceptions are a convenient way to handle errors in programs but some critics argue that they encourage laziness and are too hard to reason about. One alternative, recently explored in the Go language, is to make functions return a result together with an error code. This forces a programmer to deal with them at every calls site. Let’s imagine such an approach in OCaml by looking at what programming with lists would look like.

Every function that can fail returns a result value, indicating either the result using Ok, or a problem using Error. This is simple enough for basic list functions.

The code below is presented as a literate program. Named code snippets are introduced using <<name>>= and referred to by <<name>>. This is meta notation and not part of the actual source code. I admit that this can be confusing as monadic code also uses >>= as an infix operator.

<<type result>>=
type 'a result
= Ok of 'a
| Error of string
let return x  = Ok x  (* could have also been named "ok" *)
let error msg = Error msg

<<list functions>>=
let hd = function (* 'a list -> 'a result *)
| x::_ -> return x
| [] -> error "hd empty list"
let tl = function (* 'a list -> 'a list result *)
| [] -> error "tl empty list"
| _::xs -> return xs
let null = function (* 'a list -> bool result *)
| [] -> return true
| _ -> return false

Some functions, like null, can’t fail and we still use a result value for consistency, but this is a design choice. We can put these together to implement a length function. Before we do, here is a simple implementation that relies on exceptions:

<<Classic>>=
let rec length = function (* 'a list -> int *)
| [] -> 0
| x::xs -> 1 + length xs

And here is the implementation that relies on explicit error handling. For demonstration purposes we don’t use pattern matching for deconstructing a list but use null, hd, and tl.

<<List>>=
<<type result>>
<<list functions>>
let rec length xs = (* 'a list -> int result *)
match null xs with
| Ok true -> Ok 0
| Ok false ->
( match tl xs with
| Ok xs ->
( match length xs with
| Ok n -> Ok (n+1)
| Error msg -> Error msg
)
| Error msg -> Error msg
)
| Error msg -> Error msg

At every step we need to check whether an error has occurred. If so, we return it. Otherwise we take the value and carry on. What used to be simple is now dominated by so much error handling that it is difficult to make out the simple underlying algorithm.

Error Handling Revisited

The key observation to a better solution is that we are composing functions and match the intermediate result as part of the composition. When we abstract this into an infix operator, we regain clarity. This operator is called bind and usually bound to >>=:

<<Error>>=
<<type result>>
let bind m f = (* 'a result -> ('a -> 'b result) -> 'b result *)
match m with
| Ok x -> f x
| Error msg -> Error msg
let (>>=) = bind (* left associative *)

Function bind has type ‘a result -> (‘a -> ‘b result) -> ‘b result:

  • bind takes as first argument an intermediate result m which it inspects. If it is Ok(a), it calls f with value a. Otherwise it passes on the error unchanged.
  • bind takes as second argument a function that takes a plain value but returns a result value.

The magic of bind is put to work when used as an infix operator that receives the intermediate result on the left and the function on the right. The length function becomes:

<<Error>>=
<<list functions>>
let rec length xs = (* 'a list -> int result *)
null xs >>= fun zero ->
if zero then
return 0
else
tl xs >>= fun xs' ->
length xs' >>= fun n ->
return (n+1)

Function null provides a bool result value that is unwrapped by >>= and bound to zero when there is no error. Otherwise (invisibly) the error is passed on. Likewise, tl returns the tail of list xs as ‘a list resultand it is bound to xs’ such that it can be passed to length recursively.

When taking advantage of pattern matching, the code can be further simplified.

<<Error>>=
let rec length xs = (* 'a list -> int result *)
null xs >>= function
| true -> return 0
| false ->
tl xs >>= fun xs' ->
length xs' >>= fun n ->
return (n+1)

In a real program we would still rely on pattern matching rather than using hd, null, and tl (which I used mostly for demonstration purposes) to decompose the argument. Now length would become almost as simple as the classic implementation:

<<Error>>=
let rec length = function (* 'a list -> int result *)
| [] -> return 0
| _::xs -> length xs >>= fun n -> return (n+1)

All error handling happens invisibly inside >>= and the recurring theme is left >>= fun x -> right where the left side produces a result that is unwrapped, bound to x, and consumed by right, which in turn will produce a result value. The simplist way to produce such a result is by using return.

The Error Monad

The type result introduced above is a monad and commonly known as the error monad. A monad is defined by ingredients that we already know:

<<Monad>>=
type 'a t
val return: 'a -> 'a t
val bind: 'a t -> ('a -> 'b t) -> 'b t
  • The monad ‘a t (we used ‘a result above)
  • Function return that turns a value into a monadic value
  • Function bind to compose computations
  • Additional, domain-specific functions

Repeating the definition of the error monad in the more general notation above, it becomes:

<<ErrorMonad>>=
type 'a t
= Ok of 'a
| Error of string
let return a = Ok a
let bind m f = match m with
| Ok a -> f a
| Error msg -> Error msg

In addtion, return and bind must be well behaved in their interaction. We will not talk about what this means here. Nonetheless, I hope that the error monad demonstrates what a monad is: a clever way to compose functions. For this reason, Monads have also been described as “programmable semikolon”.

One tip: when reading monadic code that contains return and >>= it is usually best not to think about what these functions do in detail and to focus on the other parts precisely because >>= abstracts something that we should not need to know in detail.

Extended Example

What could go wrong? Create a directory with a given set of permissions and ownership. And if it exists already, fix permissions and ownership when necessary or fail otherwise while giving a good reason.

As it turns out, this requires at least 7 system calls: stat, mkdir, getgrnam, getpwnam, rmdir, chmod, chown and each can go wrong. The task concisely outlined above is easily burried in a maze of if-then-else, exception handling, and error reporting.

I am discussing the problem and a monadic solution in a blog post on Medium that extends the theme that we have seen here. The heart of the solution is function mk that employs the error monad:

<<mkdir.mli>>=
type 'a t = Ok of 'a | Error of string
val at
: path:string (* "/some/path" *)
-> perm:int (* 0o775 *)
-> user:string (* "root" *)
-> group:string (* "wheel" *)
-> unit t

Function Mkdir.at creates a directory with a given set of permissions and ownership. On success it will return an Ok () value and Error msg otherwise.

Below is the gist of the implementation that we should be able to read even without knowing all the details:

<<mkdir.ml>>=
let (>>=) = on_success
let (//=) = on_error
<<error and other basic function>>
<<defintion of always, bound to //*>>
<<checks and predicates>>
<<system calls>>
let mk path perm user group =
getgid group >>= fun gid ->
getuid user >>= fun uid ->
stat path >>= function
| Some st -> (* path already exists *)
is_dir st >>= fun () ->
(has_owner uid st
//= fun _ -> chown path uid gid) >>= fun () ->
(has_perm perm st
//= fun _ -> chmod path perm) >>= fun () ->
(has_group gid st
//= fun _ -> chown path uid gid)
(* improve error message, if we have an errror *)
//= fun msg -> error "fixing existing %s failed: %s" path msg
| None -> (* path does not exist *)
mkdir path perm >>= fun () ->
(chown path uid gid
//= (fun msg -> rmdir path //* fun () -> fail msg))
(* improve error message, if we have an error *)
//= fun msg -> error "creating %s failed: %s" path msg
let at ~path ~perm ~user ~group =
try (* just to avoid any surprises *)
mk path perm user group
with e ->
error "error creating '%s': %s" path (Printexc.to_string e)

Source Code

This section just pulls all the code snippets above together into one file such that it can be compiled and used interactively. The code is available from https://gist.github.com/lindig/b2a239514711667906e4052031ae20ec.

<<monads.ml>>=
module type Monad = sig
<<Monad>>
end
module ErrorMonad : Monad = struct
<<ErrorMonad>>
end
module List = struct
<<List>>
end
module Error = struct
<<Error>>
end
module Classic = struct
<<Classic>>
end