Specific reason for not embracing the use of exceptions for error propagation?


The primary value of avoiding exceptions is that it makes error behavior explicit in the type of the function. If you’re in an environment where everything might fail, being explicit about it is probably a negative. But if most of your function calls are total, then knowing which ones might fail highlights places where you should consider what the correct behavior is in the case of that failure. Remember that the failure of an individual step in your program doesn’t generally mean the overall failure of your code.

It’s a little bit like null-handling in languages without options. If everything might be null, well, option types probably don’t help you. But if most of the values you encounter in your program are guaranteed to be there, then tracking which ones might be null be tagging them as options is enormously helpful, since it draws your attention to the cases where it might be there, and so you get an opportunity to think about what the difference really is.

A very prosaic example. In the stdlib, Map.find throws an exception. In Base and Core, Map.find returns an Or_error.t. To me, the latter is clearly preferable, since it encourages the user to think about what they should do if Map.find fails.

Also: recovering from exceptions is tricky. An exception that arrives in the middle of some imperative part of your program may trash your state by interrupting an update that wasn’t supposed to be interrupted. If your program is a compiler that is just going to shut down in that case, then that’s probably OK. But if your program is a long-lived system that wants to handle and recover from an exception, then it’s less reasonable to just keep soldiering on without understanding where precisely that exception occurred, particularly if the correctness of this program really matters.

I should say: I don’t mean to say that one should avoid exceptions entirely; there are definitely contexts where I think they’re the right way to go. But the value in having types capture error behavior seems very clear to me.



as @bobbypriambodo explained, you don’t need to make g aware of the result type of h, by externalizing the processing of errors. That’s also a strong argument against the exception system because it leaks information between the functions and it’s against composability and modularity. Each time a new g needs to be written, the writer must remember that there are exceptions passing by in this particular use of the functions and that could make the code of g more complicated that needs to be because exception processing must be taken into account: now g must assume that the flow of execution will be interrupted and protect against that risk (close opened files,…).

The good point for error type is that the assumptions on the processing of errors by the stages of calls to functions are made clear and formal by the type system. The seperation of concerns for the processing in g can really be enforced.


I have a specific reason that only occasionally applies - laziness can obscure the times at which is is possible for an exception to be raised, making it annoyingly easy to get handling them wrong.

This is not a problem for fail-fast things like division by zero, index out of bounds, etc, but for exceptions as deliberate control flow it can be error prone.


To me there’s a big difference between having a single type to represent errors such as exn, Error.t or string, and a new error type for each function that adds or removes errors. It’s what I expect when I hear about the benefits of having errors in the function signature. A possible interface is the following:

type f_error = [ `F_invalid_arg ]
type g_error = [ `G_invalid_arg of float | `G_timeout ]
type h_error = [ `F_invalid_arg | `G_invalid_arg of float | `G_timeout ]
type i_error = [ `F_invalid_arg | `G_invalid_arg of float ]

val f : unit -> (int, f_error) result
val g : unit -> (float, g_error) result
val h : unit -> (string, h_error) result
val i : unit -> (string, i_error) result

A corresponding implementation would be:

type f_error = [ `F_invalid_arg ]
type g_error = [ `G_invalid_arg of float | `G_timeout ]
type h_error = [ f_error | g_error ]
type i_error = [ `F_invalid_arg | `G_invalid_arg of float ]

let f () =
  Ok 2

let g () =
  Error (`G_invalid_arg (-1.))

(* Merge sets of possible errors. *)
let h () =
  match f (), g () with
  | Ok a, Ok b -> Ok (Printf.sprintf "%i %g" a b)
  | Error e, _ -> Error (e :> h_error)
  | _, Error e -> Error (e :> h_error)

(* Recover from some errors. *)
let rec i () =
  match h () with
  | Ok s -> Ok s
  | Error `G_timeout -> i ()
  | Error (#i_error as e) -> Error e

I don’t know how practical this is.


My experience with using polymorphic variants for tracking more specific errors has not been good — it’s generally led to weird and confusing type error messages, and the precision hasn’t been all that useful.

I think the most prosaic examples illustrate the point best: Map.find doesn’t have a terribly unique error message, but the fact that you know that it can fail is enough of a clue to let you look deeper, and consider why it might fail. Error monads are occasionally useful, but I think most of the time, the key value of returning errors in the type is that you can see which functions don’t return errors.


I’m on board with giving the programmer a hint that a function might fail, but how do you avoid contaminating the type of other functions? An immediate failwith?

In the following code, f1 and f2 return an Or_error.t even though their caller can do nothing to prevent it. Now our codebase has 3 functions whose return types tell the programmer to be careful but there’s nothing to be careful about when calling the last two.

let divide a b =
  if b = 0 then
    Error (Error.of_string "Division by zero")
    Ok (a / b)

let f1 () =
  divide 0 0

let f2 () =
  f1 ()

So, the way I’d write my code is:

let divide a b =
  if b = 0 then
    Error (Error.of_string "Division by zero")
    Ok (a / b)

let f1 () =
  match divide 0 0 with
  | Ok x -> x
  | Error e -> Error.raise e

let f2 () =
  f1 ()

Now both f1 and f2 return a plain int and the programmer’s attention is drawn to divide. Is this recommended usage, as opposed to monadic propagation?


For starters it would help not to see it as “contamination”. You have a computation whose result maybe not only be a valid value, but also an error. That creates a complex type.

Converting an Error.t or Result.t to an exception, seems to get the worst of both approaches, unless you really have to do such a conversion due to some API or whatever.

Generally you need to either propagate the error to another area of the code base that knows how to deal with such error (e.g. division by zero could request the user to enter it again). Monads and functors help you do that (e.g. >>=).

Another thing you could do is have a mandatory or optional function parameter which knows how to deal with various errors (e.g. division ~error_handler a b). I said this in the beginning of the thread… this really depends on the specifics of each problem and algorithm.

If you get a chance try both approaches and see what happens in the long run.


Exactly. That’s what exception propagation does. Without monads or functors.

Been there, done that. The most usable pattern was Http_exn.not_found "no such user account".


Yes, and at the cost of sacrificing guarantees from the type system.


(If that’s seems like the right trade off-for your use case, then by all means use exceptions.)


I think we need to distinguish the case when a function cannot be evaluated at a point because the arguments are outside the domain of the function and the case when evaluation of the function at a possibly valid point of it’s domains cannot be carried due to external factors. Examples of external factors would be external services not being available or misbehaving and resource exhaustion. I would only consider the latter case of errors true runtime errors, and a prime use case for result, since such errors are outside the control of the code, and generally should be handled.

What about domain errors then? In general I would tend to classify these as bugs, but there may be cases where it’s safer or more convenient to use result or option rather than raising exceptions. Some examples where I would prefer exceptions:

  • Division by zero. The code which passed the zero is most likely broken. If the argument is a float, this is easily seen by adding a noise to the divisor. If the program relies on catching division by zero, it will instead receive huge numbers which may invalidate the result.
  • More generally all floating and integer point functions which correspond to mathematical functions. E.g. there is a good practise in analysis to carefully specify domains, and if the code is correct it stays within those domains.
  • I think the practise of making sure to stay within the domain is preferable for pure functions as far as feasible (see below). A symptom of a result which should have been an exception is that the users can clearly see something is wrong and terminates the Error case with an assert false.

But what about the danger that the code fails with an uncaught exception? Well, isn’t there an even greater risk that the code just makes a wrong calculation on some parts of its domain? Note in contrast, that true runtime errors can occur no matter how correct the code is, so it makes a lot more sense to force the caller to deal with it.

Still, one reason to use result for domain errors may be that the structure of the part of the domain on which the function is defined is too complex for the caller to be expected to deal with. I typical example would be conversions and parsers. If inputs come from external sources, it should be checked, but the easiest way to check the input may be to actually try to parse it.

What about find then? This can be seen as another case of a complex domain, since there is a non-trivial dependency between the container and the search key (though of course one can use mem to do the check). But another resolution of this case, which I think is more elegant, is to not consider a negative lookup to be an error at all, but to rather expose the map as an explicitly partial function. That is, to let find return an option instead of a result. The justification for this view would be if uncaught Not_found exceptions tend to indicate that the code it fact needs a representation of a partial function.


Which guarantees are you referring to?

In the following code (from earlier message), the type system does not tell you which function body might contain a bug because they all return Or_error.t instead of just the sensitive function divide:

let divide a b =
  if b = 0 then
    Error (Error.of_string "Division by zero")
    Ok (a / b)

let f1 () =
  divide 0 0

let f2 () =
  f1 ()


Maybe we need a slightly bigger example than division. A while ago I have explored a seemingly simple exercise in error handling:

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.

The functions in the Unix module return exceptions but I found it easier to implement this using an error monad. In particular, I wanted to have sensible error messages. Maybe this can be also done with exceptions and I would be interested to read about it.

My writeup is here: Making a Directory in OCaml.



  • you have a function f : a -> b
  • the code of f is “pure” (which I roughly take to mean it does not use exceptions, and all functions and operators it uses are also pure)

then the type system guarantees that f will either (i) terminate with a value of type b, or (ii) diverge.
If on the other hand f is “impure” (i.e., it uses exceptions) then there is a third possibility: (iii) f may throw an arbitrary exception.

You could imagine an even stronger notion of “purity” where the type system rules out case (ii) as well (and in fact such type systems are used in proof assistants like Coq and Isabelle). The stronger your notion of “purity”, the stronger the guarantees your type system can provide. Another popular notion of “purity” (for example in Haskell) is no state & no IO. Then the type system can rule out nondeterminism.


Sorry, I see code bloat where you see purity.


What kind of guarantee is that? Removing exceptions from the system and having every function return Ok_error.t adds no information to the program. One form of the program can be trivially converted to the other, which is why we prefer the form that’s shorter, i.e. the one using exceptions.

This leaves room for using Or_error.t on some functions, while still using exceptions. See earlier posts.


I did not make any judgment on whether purity is a good thing or not. I just gave you the facts. You can do with them whatever floats your boat.


The trick is that in this translation, you can leave pure functions unmodifiee and have them return exactly the same thing as before, not an Or_error.t. Only impure functions have to be modified in the way you describe. And now you get the extra benefits that I described above. (Of course if you have no pure functions, you indeed win nothing.)

EDIT: In particular, you may make your “main function” pure by handling potential Or_error.t’s, if there are any. Then you’re guaranteed to never see uncaught exceptions.


Just for the record, I have no problem with impurity and have never used Or_error.t or Result.t before because it doesnt seem worth it for my purposes.


What is the purpose of f2? If f2 is a public interface that provides the capability of dividing integers, then returning an Or_error.t is intended because it could fail. This example might be too small and too abstract to gain any conclusion. And for your earlier post:

If the only function that can fail is divide, then yes. But imagine f1 (again, I’m afraid that using an imaginary function name like this might make it harder to deliver my point) calls other functions that can also fail. You would then end up with:

let f1 () =
  match divide 0 0 with
  | Error e -> Error.raise e
  | Ok x ->
    match do_something x with
    | Error e -> Error.raise e
    | Ok (y, z) ->
      match do_another_thing z with
      | Error e -> Error.raise e
      | Ok fin -> fin + y

Monadic constructs such as bind (>>=) can help simplifying this kind of code into:

let f1 () =
  let final_result =
    divide 0 0 >>= fun x ->
    do_something x >>= fun (y, z) ->
    do_another_thing z >>= fun fin ->
    fin + y
  match final_result with
  | Error e -> Error.raise e
  | Ok v -> v

So it’s the choice of whether to handle or to propagate, and determining when to do which is deeply related to the nature and contract of the functions and what you’re trying to do.