On Effectiveness of Exceptions in OCaml
Tags: ocaml
Published: 2020-12-03 18:24:25

# Why I Use Exceptions in OCaml

I attempted to pervasively use `result` types as an error denoting mechanism in my OCaml projects. However, my experience suggests that it is an inadequate replacement for the venerable `exn` type in ocaml.
I list some of my observations below.

**Note**: This is an experience report and not a recommendation for you to use either an exception or a `result` type.
Perhaps this could serve as a data point to the similar question that I asked myself. The question of whether to use `exception` or `result` type as an error denotation technique in OCaml software development.

## Error reporting

OCaml goes a long, long way towards ensuring that your software is correct by construction.
However, errors are a fact of life in software and we need a good mechanism to deal with it.
Such mechanism should allow us to efficiently, correctly and accurately identify the source of our errors.

OCaml exception back traces - or call/stack traces - is one such tool which I have found very helpful. It gives the offending file and the line number in it. This make investigating and zeroing in on the error efficient and productive.

Using `result` type means you lose this valuable utility that is in-built to the ocaml language and the compiler.

## Error Localisation, Re-use and API Ergonomics

Let's say we have module `A` defined as such in some ocaml library `lib_a`,

```ocaml
module A : sig
  type error = [ `Divisor ]

  val divide : int -> int -> (int, [> error ]) result
end = struct
  type error = [ `Divisor ]
  let divide a b = if b <= 0 then Error `Divisor else Ok (a / b)
end
```

Let's assume I am developing another ocaml libary `lib_b` which has module `B` defined as below. `lib_b` uses `lib_a` and consequently module `A`.

However, the module `B` doesn't compile,

```ocaml
(* Module B doesn't compile. *)
module B : sig
  type error = [`Some_error]

  val mult_div : int -> int -> (int, [> error]) result
end = struct
  type error = [`Some_error]

  let mult_div a b =
    Stdlib.Result.bind (A.divide a b)
    (fun c ->
      if c < 10 then Error `Some_error else Ok (c * 10))
end
```

In order for module `B` to compile you have to forgo `` type error = [`Some_error] `` declaration in module `B`, like so,

```ocaml
module B1 : sig
  val mult_div : int -> int -> (int, [> `Some_error | `Divisor ]) result
end = struct
  let mult_div a b =
    Stdlib.Result.bind (A.divide a b) (fun c ->
        if c < 10 then Error `Some_error else Ok (c * 10))
end
```

Perhaps we don't mind type annotation so much in a function signature. However, this doesn't scale if you are using multiple libraries in `B1` and each have specific error which you now have to surface.

Additionally, we have now lost even more error localisation mechanism, i.e. `Divisor` seems to be erroring out in `B1.mult_div` but is it really?

Perhaps the disadvantage with the loss of error localisation can somehow be mitigated below as such,

```ocaml
module B2 : sig
  type error = [ `A of A.error | `Some_error ]

  val mult_div : int -> int -> (int, [> error ]) result
end = struct
  type error = [ `A of A.error | `Some_error ]

  let mult_div a b =
    match A.divide a b with
    | Ok c -> if c < 10 then Error `Some_error else Ok (c * 10)
    | Error e -> Error (`A e)
end
```

However, this suffers from the same scalablity defect as `B1`. Your `error` will be the concatenation of all the errors you use in `mult_div`.

Additionally, users of `B2` will have to define their own error type which again wraps `B2.error`. Perhaps like so,

```ocaml
module type C = sig
  type error = [ `B of B2.error | `Error_c ]

  val mult_div : int -> int -> (int, [> error ]) result
end
```

We have now built a hierarchy of errors. Such API didn't feel ergonomic to me.

## Iterative Development and Focus on Error

I prefer iterative development. For example, I write a function, save it and load it up in utop and explore a range of input and outputs of the function. At this exploratory/iterative stage, I don't want to focus too much on the error cases or conditions.

Using `result` means you probably have to devote some attention to its error conditions. This is probably okay. However, the code may also be a throw-away code. At the initial stages of development it is more than likely. So your iteration velocity may be impacted. See above point regarding error localisation when using `result` type.

With exceptions - because it gives line and file name locations of your error - you don't have to spend too much time trying to figure out where exactly the error occurred in your code. This increases your iteration velocity and bottom up development. That is, you quickly discover an error scenario and use that to make you abstraction and encapsulation more robust. Loop and Repeat.

## Correct By Construction

I thought this would be the biggest advantage of using `result` type and a net benefit.
However, my experience of **NOT** using it didn't result in any noticeable reduction of _correct by construction_ OCaml software.
Conversely, I didn't notice any noticeable improvement on this metric when **using** it.
What I have noticed over time is that abstraction/encapsulation mechanisms and type system in particular play by far the most significant role in creating **correct by construction** OCaml software.

## Summary

Current sentiments in OCaml development seem to emphasise the use of `result` as an error mechanism. However after attempting to use it pervasively, I found it lacking. Specifically as a means of denoting errors in your OCaml software.