Chapter 7. Error Handling
Nobody likes dealing with errors. It's tedious, it's easy to get wrong, and it's usually just not as fun as planning out how your program is going to succeed. But error handling is important, and however much you don't like thinking about it, having your software fail due to poor error handling is worse.
Thankfully, OCaml has powerful tools for handling errors reliably and with a minimum of pain. In this chapter we'll discuss some of the different approaches in OCaml to handling errors, and give some advice on how to design interfaces that make error handling easier.
Error-Aware Return Types
The best way in OCaml to signal an error is to include that error in
your return value. Consider the type of the
find function in the
# List.find;;- : 'a list -> f:('a -> bool) -> 'a option = <fun>
The option in the return type indicates that the function may not succeed in finding a suitable element:
# List.find [1;2;3] ~f:(fun x -> x >= 2) ;;- : int option = Some 2# List.find [1;2;3] ~f:(fun x -> x >= 10) ;;- : int option = None
Including errors in the return values of your functions requires the caller to handle the error explicitly, allowing the caller to make the choice of whether to recover from the error or propagate it onward.
function. The function takes a list and a comparison function and returns
upper and lower bounds for the list by finding the smallest and largest
element on the list.
List.last, which return
None when they encounter an empty list, are used
to extract the largest and smallest element of the list:
# let compute_bounds ~cmp list = let sorted = List.sort ~cmp list in match List.hd sorted, List.last sorted with | None,_ | _, None -> None | Some x, Some y -> Some (x,y) ;;val compute_bounds : cmp:('a -> 'a -> int) -> 'a list -> ('a * 'a) option = <fun>
match statement is used to handle the error
cases, propagating a
into the return value of
On the other hand, in the
find_mismatches that follows, errors encountered
during the computation do not propagate to the return value of the
find_mismatches takes two
hash tables as arguments and searches for keys that have different data in
one table than in the other. As such, the failure to find a key in one
table isn't a failure of any sort:
# let find_mismatches table1 table2 = Hashtbl.fold table1 ~init: ~f:(fun ~key ~data mismatches -> match Hashtbl.find table2 key with | Some data' when data' <> data -> key :: mismatches | _ -> mismatches ) ;;val find_mismatches : ('a, 'b) Hashtbl.t -> ('a, 'b) Hashtbl.t -> 'a list = <fun>
The use of options to encode errors underlines the fact that it's not clear whether a particular outcome, like not finding something on a list, is an error or is just another valid outcome. This depends on the larger context of your program, and thus is not something that a general-purpose library can know in advance. One of the advantages of error-aware return types is that they work well in both situations.
Encoding Errors with Result
Options aren't always a sufficiently expressive way to report
errors. Specifically, when you encode an error as
None, there's nowhere to say anything about
the nature of the error.
module Result : sig type ('a,'b) t = | Ok of 'a | Error of 'b end
Result.t is essentially an
option augmented with the ability to store other information in the
error case. Like
None for options, the constructors
Error are promoted to the toplevel by
Core.Std. As such, we can write:
# [ Ok 3; Error "abject failure"; Ok 4 ];;- : (int, string) Result.t list = [Ok 3; Error "abject failure"; Ok 4]
without first opening the
Error and Or_error
Result.t gives you complete
freedom to choose the type of value you use to represent errors, but
it's often useful to standardize on an error type. Among other things,
this makes it easier to write utility functions to automate common error
But which type to choose? Is it better to represent errors as strings? Some more structured representation like XML? Or something else entirely?
Core's answer to this question is the
Error.t type, which tries to forge a good
compromise between efficiency, convenience, and control over the
presentation of errors.
It might not be obvious at first why efficiency is an issue at all. But generating error messages is an expensive business. An ASCII representation of a value can be quite time-consuming to construct, particularly if it includes expensive-to-convert numerical data.
Error gets around this issue
through laziness. In particular, an
Error.t allows you to put off generation of
the error string until and unless you need it, which means a lot of the
time you never have to construct it at all. You can of course construct
an error directly from a string:
# Error.of_string "something went wrong";;- : Error.t = something went wrong
# Error.of_thunk (fun () -> sprintf "something went wrong: %f" 32.3343);;- : Error.t = something went wrong: 32.334300
In this case, we can benefit from the laziness of
Error, since the thunk won't be called unless
Error.t is converted to a
S-expressions are supported by the Sexplib package that is
distributed with Core and is the most common serialization format used
in Core. Indeed, most types in Core come with built-in s-expression
converters. Here's an example of creating an error using the sexp
converter for times,
# Error.create "Something failed a long time ago" Time.epoch Time.sexp_of_t;;- : Error.t = Something failed a long time ago: (1969-12-31 19:00:00.000000-05:00)
Note that the time isn't actually serialized into an s-expression until the error is printed out.
We're not restricted to doing this kind of error reporting with built-in types. This will be discussed in more detail in Chapter 17, Data Serialization with S-Expressions, but Sexplib comes with a language extension that can autogenerate sexp converters for newly generated types:
# let custom_to_sexp = <:sexp_of<float * string list * int>>;;val custom_to_sexp : float * string list * int -> Sexp.t = <fun># custom_to_sexp (3.5, ["a";"b";"c"], 6034);;- : Sexp.t = (3.5 (a b c) 6034)
We can use this same idiom for generating an error:
# Error.create "Something went terribly wrong" (3.5, ["a";"b";"c"], 6034) <:sexp_of<float * string list * int>> ;;- : Error.t = Something went terribly wrong: (3.5(a b c)6034)
Error also supports operations
for transforming errors. For example, it's often useful to augment an
error with information about the context of the error or to combine
multiple errors together.
Error.of_list fulfill these
# Error.tag (Error.of_list [ Error.of_string "Your tires were slashed"; Error.of_string "Your windshield was smashed" ]) "over the weekend" ;;- : Error.t = over the weekend: Your tires were slashed; Your windshield was smashed
'a Or_error.t is just
a shorthand for
Result.t, and it is, after
option, the most common way of returning
errors in Core.
bind and Other Error Handling Idioms
As you write more error handling code in OCaml, you'll discover
that certain patterns start to emerge. A number of these common patterns
have been codified by functions in modules like
Result. One particularly useful pattern is
built around the function
is both an ordinary function and an infix operator
>>=. Here's the definition of
bind for options:
# let bind option f = match option with | None -> None | Some x -> f x ;;val bind : 'a option -> ('a -> 'b option) -> 'b option = <fun>
As you can see,
bind None f returns
None without calling
bind (Some x) f returns
bind can be used as a way of sequencing
together error-producing functions so that the first one to produce an error terminates the
computation. Here's a rewrite of
compute_bounds to use a
nested series of
# let compute_bounds ~cmp list = let sorted = List.sort ~cmp list in Option.bind (List.hd sorted) (fun first -> Option.bind (List.last sorted) (fun last -> Some (first,last))) ;;val compute_bounds : cmp:('a -> 'a -> int) -> 'a list -> ('a * 'a) option = <fun>
The preceding code is a little bit hard to swallow, however, on a
syntactic level. We can make it easier to read and drop some of the
parentheses, by using the infix operator form of
bind, which we get access to by locally opening
Option.Monad_infix. The module is
Monad_infix because the
bind operator is part of a subinterface called
Monad, which we'll see again in Chapter 18, Concurrent Programming with Async:
# let compute_bounds ~cmp list = let open Option.Monad_infix in let sorted = List.sort ~cmp list in List.hd sorted >>= fun first -> List.last sorted >>= fun last -> Some (first,last) ;;val compute_bounds : cmp:('a -> 'a -> int) -> 'a list -> ('a * 'a) option = <fun>
This use of
bind isn't really
materially better than the one we started with, and indeed, for small
examples like this, direct matching of options is generally better than
bind. But for large, complex
examples with many stages of error handling, the
idiom becomes clearer and easier to manage.
There are other useful idioms encoded in the functions in
Option. One example is
Option.both, which takes two optional values
and produces a new optional pair that is
None if either of its arguments are
Option.both, we can make
compute_bounds even shorter:
# let compute_bounds ~cmp list = let sorted = List.sort ~cmp list in Option.both (List.hd sorted) (List.last sorted) ;;val compute_bounds : cmp:('a -> 'a -> int) -> 'a list -> ('a * 'a) option = <fun>
These error-handling functions are valuable because they let you
express your error handling both explicitly and concisely. We've only
discussed these functions in the context of the
Option module, but more functionality of this
kind can be found in the
Exceptions in OCaml are not that different from exceptions in many other languages, like Java, C#, and Python. Exceptions are a way to terminate a computation and report an error, while providing a mechanism to catch and handle (and possibly recover from) exceptions that are triggered by subcomputations.
You can trigger an exception by, for example, dividing an integer by zero:
# 3 / 0;;Exception: Division_by_zero.
And an exception can terminate a computation even if it happens nested somewhere deep within it:
# List.map ~f:(fun x -> 100 / x) [1;3;0;4];;Exception: Division_by_zero.
If we put a
printf in the middle
of the computation, we can see that
List.map is interrupted partway through its
execution, never getting to the end of the list:
# List.map ~f:(fun x -> printf "%d\n%!" x; 100 / x) [1;3;0;4];;
1 3 0 Exception: Division_by_zero.
In addition to built-in exceptions like
Divide_by_zero, OCaml lets you define your
# exception Key_not_found of string;;exception Key_not_found of string# raise (Key_not_found "a");;Exception: Key_not_found("a").
Exceptions are ordinary values and can be manipulated just like other OCaml values:
# let exceptions = [ Not_found; Division_by_zero; Key_not_found "b" ];;val exceptions : exn list = [Not_found; Division_by_zero; Key_not_found("b")]# List.filter exceptions ~f:(function | Key_not_found _ | Not_found -> true | _ -> false);;- : exn list = [Not_found; Key_not_found("b")]
Exceptions are all of the same type,
type is something of a special case in the OCaml type system. It is
similar to the variant types we encountered in Chapter 6, Variants, except that it is open,
meaning that it's not fully defined in any one place. In particular, new
tags (specifically, new exceptions) can be added to it by different parts
of the program. This is in contrast to ordinary variants, which are
defined with a closed universe of available tags. One result of this is
that you can never have an exhaustive match on an
exn, since the full set of possible exceptions
is not known.
The following function uses the
Key_not_found exception we defined above to
signal an error:
# let rec find_exn alist key = match alist with |  -> raise (Key_not_found key) | (key',data) :: tl -> if key = key' then data else find_exn tl key ;;val find_exn : (string * 'a) list -> string -> 'a = <fun># let alist = [("a",1); ("b",2)];;val alist : (string * int) list = [("a", 1); ("b", 2)]# find_exn alist "a";;- : int = 1# find_exn alist "c";;Exception: Key_not_found("c").
In the preceding example,
throws the exception, thus terminating the computation. The type of raise
is a bit surprising when you first see it:
# raise;;- : exn -> 'a = <fun>
The return type of
'a makes it
raise manufactures a value to
return that is completely unconstrained in its type. That seems
impossible, and it is. Really,
has a return type of
'a because it
never returns at all. This behavior isn't restricted to functions like
raise that terminate by throwing
exceptions. Here's another example of a function that doesn't return a
# let rec forever () = forever ();;val forever : unit -> 'a = <fun>
forever doesn't return a value
for a different reason: it's an infinite loop.
This all matters because it means that the return type of
raise can be whatever it needs to be to fit into
the context it is called in. Thus, the type system will let us throw an
exception anywhere in a program.
Helper Functions for Throwing Exceptions
# let failwith msg = raise (Failure msg);;val failwith : string -> 'a = <fun>
There are several other useful functions for raising exceptions,
which can be found in the API documentation for the
Exn modules in Core.
Another important way of throwing an exception is the
assert is used for situations where a
violation of the condition in question indicates a bug. Consider the
following piece of code for zipping together two lists:
# let merge_lists xs ys ~f = if List.length xs <> List.length ys then None else let rec loop xs ys = match xs,ys with | , ->  | x::xs, y::ys -> f x y :: loop xs ys | _ -> assert false in Some (loop xs ys) ;;val merge_lists : 'a list -> 'b list -> f:('a -> 'b -> 'c) -> 'c list option = <fun># merge_lists [1;2;3] [-1;1;2] ~f:(+);;- : int list option = Some [0; 3; 5]# merge_lists [1;2;3] [-1;1] ~f:(+);;- : int list option = None
Here we use
assert false, which
means that the
assert is guaranteed to trigger. In
general, one can put an arbitrary condition in the assertion.
In this case, the
assert can never be triggered
because we have a check that makes sure that the lists are of the same
length before we call
loop. If we
change the code so that we drop this test, then we can trigger the
# let merge_lists xs ys ~f = let rec loop xs ys = match xs,ys with | , ->  | x::xs, y::ys -> f x y :: loop xs ys | _ -> assert false in loop xs ys ;;val merge_lists : 'a list -> 'b list -> f:('a -> 'b -> 'c) -> 'c list = <fun># merge_lists [1;2;3] [-1] ~f:(+);;Exception: (Assert_failure //toplevel// 5 13).
This shows what's special about
assert: it captures the line number and
character offset of the source location from which the assertion was
So far, we've only seen exceptions fully terminate the execution of a computation. But often, we want a program to be able to respond to and recover from an exception. This is achieved through the use of exception handlers.
In OCaml, an exception handler is declared using a
statement. Here's the basic syntax.
try <expr> with | <pat1> -> <expr1> | <pat2> -> <expr2> ...
try/with clause first evaluates its body,
expr. If no exception is thrown,
then the result of evaluating the body is what the entire
try/with clause evaluates to.
But if the evaluation of the body throws an exception, then the
exception will be fed to the pattern-match statements following the
with. If the exception matches a
pattern, then we consider the exception caught, and the
try/with clause evaluates to the expression on
the righthand side of the matching pattern.
Otherwise, the original exception continues up the stack of function calls, to be handled by the next outer exception handler. If the exception is never caught, it terminates the program.
Cleaning Up in the Presence of Exceptions
One headache with exceptions is that they can terminate your execution at unexpected places, leaving your program in an awkward state. Consider the following function for loading a file full of reminders, formatted as s-expressions:
# let reminders_of_sexp = <:of_sexp<(Time.t * string) list>> ;;val reminders_of_sexp : Sexp.t -> (Time.t * string) list = <fun># let load_reminders filename = let inc = In_channel.create filename in let reminders = reminders_of_sexp (Sexp.input_sexp inc) in In_channel.close inc; reminders ;;val load_reminders : string -> (Time.t * string) list = <fun>
The problem with this code is that the function that loads the
s-expression and parses it into a list of
string pairs might throw an exception if the
file in question is malformed. Unfortunately, that means that the
In_channel.t that was opened will
never be closed, leading to a file-descriptor leak.
We can fix this using Core's
protect function, which takes two arguments: a
f, which is the main body of
the computation to be run; and a thunk
finally, which is to be called when
f exits, whether it exits normally or with an
exception. This is similar to the
try/finally construct available in many
programming languages, but it is implemented in a library, rather than
being a built-in primitive. Here's how it could be used to fix
# let load_reminders filename = let inc = In_channel.create filename in protect ~f:(fun () -> reminders_of_sexp (Sexp.input_sexp inc)) ~finally:(fun () -> In_channel.close inc) ;;val load_reminders : string -> (Time.t * string) list = <fun>
This is a common enough problem that
In_channel has a function called
with_file that automates this pattern:
# let reminders_of_sexp filename = In_channel.with_file filename ~f:(fun inc -> reminders_of_sexp (Sexp.input_sexp inc)) ;;val reminders_of_sexp : string -> (Time.t * string) list = <fun>
In_channel.with_file is built
on top of
protect so that it can
clean up after itself in the presence of exceptions.
Catching Specific Exceptions
OCaml's exception-handling system allows you to tune your
error-recovery logic to the particular error that was thrown. For
Not_found when the element in
question can't be found. Let's look at an example of how you could take
advantage of this. In particular, consider the following
# let lookup_weight ~compute_weight alist key = try let data = List.Assoc.find_exn alist key in compute_weight data with Not_found -> 0. ;;val lookup_weight : compute_weight:('a -> float) -> ('b, 'a) List.Assoc.t -> 'b -> float = <fun>
As you can see from the type,
lookup_weight takes an association list, a key
for looking up a corresponding value in that list, and a function for
computing a floating-point weight from the looked-up value. If no value
is found, then a weight of
The use of exceptions in this code, however, presents some
problems. In particular, what happens if
compute_weight throws an exception? Ideally,
lookup_weight should propagate that
exception on, but if the exception happens to be
Not_found, then that's not what will
# lookup_weight ~compute_weight:(fun _ -> raise Not_found) ["a",3; "b",4] "a" ;;- : float = 0.
This kind of problem is hard to detect in advance because the type system doesn't tell you what exceptions a given function might throw. For this reason, it's generally better to avoid relying on the identity of the exception to determine the nature of a failure. A better approach is to narrow the scope of the exception handler, so that when it fires it's very clear what part of the code failed:
# let lookup_weight ~compute_weight alist key = match try Some (List.Assoc.find_exn alist key) with _ -> None with | None -> 0. | Some data -> compute_weight data ;;val lookup_weight : compute_weight:('a -> float) -> ('b, 'a) List.Assoc.t -> 'b -> float = <fun>
At this point, it makes sense to simply use the
# let lookup_weight ~compute_weight alist key = match List.Assoc.find alist key with | None -> 0. | Some data -> compute_weight data ;;val lookup_weight : compute_weight:('a -> float) -> ('b, 'a) List.Assoc.t -> 'b -> float = <fun>
open Core.Std exception Empty_list let list_max = function |  -> raise Empty_list | hd :: tl -> List.fold tl ~init:hd ~f:(Int.max) let () = printf "%d\n" (list_max [1;2;3]); printf "%d\n" (list_max )
If we build and run this program, we'll get a stack backtrace that will provide some information about where the error occurred and the stack of function calls that were in place at the time of the error:
$ corebuild blow_up.byte$ ./blow_up.byte3Fatal error: exception Blow_up.Empty_listRaised at file "blow_up.ml", line 5, characters 16-26Called from file "blow_up.ml", line 10, characters 17-28
You can also capture a backtrace within your program by calling
Exn.backtrace, which returns the
backtrace of the most recently thrown exception. This is useful for
reporting detailed information on errors that did not cause your program
This works well if you have backtraces enabled, but that isn't always the case. In fact, by default, OCaml has backtraces turned off, and even if you have them turned on at runtime, you can't get backtraces unless you have compiled with debugging symbols. Core reverses the default, so if you're linking in Core, you will have backtraces enabled by default.
Even using Core and compiling with debugging symbols, you can turn
backtraces off by setting the
OCAMLRUNPARAM environment variable to be
$ corebuild blow_up.byte$ OCAMLRUNPARAM= ./blow_up.byte3Fatal error: exception Blow_up.Empty_list
There is a legitimate reasons to run without backtraces: speed.
OCaml's exceptions are fairly fast, but they're even faster still if you
disable backtraces. Here's a simple benchmark that shows the effect,
open Core.Std open Core_bench.Std let simple_computation () = List.range 0 10 |> List.fold ~init:0 ~f:(fun sum x -> sum + x * x) |> ignore let simple_with_handler () = try simple_computation () with Exit -> () let end_with_exn () = try simple_computation (); raise Exit with Exit -> () let () = [ Bench.Test.create ~name:"simple computation" (fun () -> simple_computation ()); Bench.Test.create ~name:"simple computation w/handler" (fun () -> simple_with_handler ()); Bench.Test.create ~name:"end with exn" (fun () -> end_with_exn ()); ] |> Bench.make_command |> Command.run
We're testing three cases here: a simple computation with no exceptions; the same computation with an exception handler but no thrown exceptions; and finally the same computation where we use the exception to do the control flow back to the caller.
If we run this with stacktraces on, the benchmark results look like this:
$ corebuild -pkg core_bench exn_cost.native$ ./exn_cost.native -ascii cyclesEstimated testing time 30s (change using -quota SECS).Name Time/Run Cycles/Run % of max------------------------------ ---------- ------------ ----------simple computation 74.43 171 71.63simple computation w/handler 92.46 213 88.98end with exn 104 239 100.00
Here, we see that we lose something like 30 cycles to adding an exception handler, and 60 more to actually throwing and catching an exception. If we turn backtraces off, then the results look like this:
$ OCAMLRUNPARAM= ./exn_cost.native -ascii cyclesEstimated testing time 30s (change using -quota SECS).Name Time/Run Cycles/Run % of max------------------------------ ---------- ------------ ----------simple computation 84.85 195 82.06simple computation w/handler 91.95 211 88.93end with exn 103 238 100.00
Here, the handler costs about the same, but the exception itself costs only 25, as opposed to 60 additional cycles. All told, this should only matter if you're using exceptions routinely as part of your flow control, which is in most cases a stylistic mistake anyway.
From Exceptions to Error-Aware Types and Back Again
Both exceptions and error-aware types are necessary parts of programming in OCaml. As such, you often need to move between these two worlds. Happily, Core comes with some useful helper functions to help you do just that. For example, given a piece of code that can throw an exception, you can capture that exception into an option as follows:
# let find alist key = Option.try_with (fun () -> find_exn alist key) ;;val find : (string * 'a) list -> string -> 'a option = <fun># find ["a",1; "b",2] "c";;- : int option = None# find ["a",1; "b",2] "b";;- : int option = Some 2
Or_error have similar
try_with functions. So, we could write:
# let find alist key = Or_error.try_with (fun () -> find_exn alist key) ;;val find : (string * 'a) list -> string -> 'a Or_error.t = <fun># find ["a",1; "b",2] "c";;- : int Or_error.t = Core_kernel.Result.Error ("Key_not_found(\"c\")")
And then we can reraise that exception:
Choosing an Error-Handling Strategy
Exceptions are more concise because they allow you to defer the job of error handling to some larger scope, and because they don't clutter up your types. But this concision comes at a cost: exceptions are all too easy to ignore. Error-aware return types, on the other hand, are fully manifest in your type definitions, making the errors that your code might generate explicit and impossible to ignore.
The right trade-off depends on your application. If you're writing a rough-and-ready program where getting it done quickly is key and failure is not that expensive, then using exceptions extensively may be the way to go. If, on the other hand, you're writing production software whose failure is costly, then you should probably lean in the direction of using error-aware return types.
To be clear, it doesn't make sense to avoid exceptions entirely. The maxim of "use exceptions for exceptional conditions" applies. If an error occurs sufficiently rarely, then throwing an exception is often the right behavior.
Also, for errors that are omnipresent, error-aware return types may be overkill. A good example is out-of-memory errors, which can occur anywhere, and so you'd need to use error-aware return types everywhere to capture those. Having every operation marked as one that might fail is no more explicit than having none of them marked.
In short, for errors that are a foreseeable and ordinary part of the execution of your production code and that are not omnipresent, error-aware return types are typically the right solution.