You’re better off using Exceptions

Exception handling is an error management paradigm that has often been met with criticism. Such criticisms typically revolve around scoping considerations, exceptions-as-control-flow abuse or even the assertion that exceptions are really just a type safe version of goto. To an extent, these seem like valid concerns but it is not within the scope of this article to address those per se.

Such concerns resonate particularly well within FP communities, often taken to the extreme: we should reject exceptions altogether, since code that throws is necessarily impure. In the F# community, this opinion is in part realized by advocating alternatives like result types and railway-oriented programming. In essence, these approaches follow the Either monad found in Haskell, but often intentionally avoiding the use of do notation/computation expressions (since that’s just interpreted exception semantics).

The TL;DR version of the approach is that we define a union type for results that looks like this:

type Result<'TSuccess, 'TError> =
    | Ok of 'TSuccess
    | Error of 'TError

then require that any function which admits the possibility of error should return a Result value. This way we are forcing the type system acknowledge error cases and makes our error handling logic more explicit and predictable. We can also use combinators like bind to encode common error handling flows.

I would like to illustrate in this article why I believe that this often causes a lot of problems — while simultaneously not achieving the advertised benefits, particularly in the context of maintaining large codebases. It also gives rise to a multitude of anti-patterns, particularly when used by people new to F#.

An issue of Runtime

Most programming languages out there carry the concept of a runtime error. Such errors sometimes stem from unexpected locations, in code otherwise considered pure. In Haskell for instance, consider what the pure expressions “2 `div` 0” or “head []” evaluate to. In the case of the CLR, runtime errors are manifested in the form of thrown exceptions, which arguably happens more often than Haskell.

Because runtime errors are difficult to anticipate, I claim that using result types as a holistic replacement for error handling in an application is a leaky abstraction. Consider the following example:

type Customer = { Id : string; Credit : decimal option }

let average (customers : Customer list) =
    match customers with
    | [] -> Error "list was empty"
    | _ ->
        |> List.averageBy (fun c -> c.Credit.Value)
        |> Ok

Notice that the above snippet now admits two possible classes of errors, string and the potential NullReferenceException admitted by careless access of the optional field in the customer record. This eventually delivers an unpleasant surprise to consumers of the function, since the type signature communicates an exception-free implementation.

An Awkward Reconciliation

It’s by such misadventure that the working F# programmer soon realizes that any function could still potentially throw. This is an awkward realization, which has to be addressed by catching as soon as possible. This of course is the purely functional equivalent of the Pokemon anti-pattern:

let avg : Result<decimal, string> =
    try average customers
    with e -> Error e.Message // gotta catch em all!


In the majority of codebases using result types that I’ve been reviewing, people typically just end up re-implementing exception semantics on an ad-hoc basis. This can result in extremely noisy code, as illustrated in the following example:

let combine x y z =
    match x, y, z with
    | Ok x, Ok y, Ok z -> Ok (x,y,z)
    | Error e, _, _ | _, Error e, _ | _, _, Error e -> Error e

or in using pre-baked combinators

let combine x y z =
    x |> bind (fun x -> y |> bind (fun y -> z |> bind (fun z -> Ok(x,y,z))))

We really shouldn’t be doing this. It is essentially the type equivalent of using a linter that demands inserting catch-alls and rethrows in every method body.

Given sufficient implementation complexity, things can get really ugly: binding on results of differing error types and other incidental complexities require substantial refactoring to get things right. Often, this will prompt developers to cut corners by doing unexpected things, like inserting a throw just to get rid of an annoying error branch or returning nested result types like

Result<Result<string, string>, string list>

Where’s my Stacktrace?

An important property of exceptions -which cannot be stressed enough- is that they are entities managed and understood by the underlying runtime, endowed with metadata critical to diagnosing bugs in complex systems. They can also be tracked and highlighted by tooling such as debuggers and profilers, providing invaluable insight when probing a large system.

By lifting all of our error handling to passing result values, we are essentially discarding all that functionality. There is nothing worse than having to deal with a production issue which comes up in the logs simply as "list was empty".

The Problem with IO

The points discussed thus far have typically focused on applications that might as well have been purely functional. As might be expected, these become even more pronounced when bringing in IO or when interfacing with third-party .NET libraries. Consider the following example:

let tryReadAllText (path : string) : string option =
    try System.IO.File.ReadAllText path |> Some
    with _ -> None

I have spotted this approach and variations in many production-grade F# codebases, and have been responsible for some of these myself back when I was younger and more naive. This adapter is motivated by the desire to have an exception-free file reader, however it is deeply flawed precisely because of that.

While it is fairly unambiguous what a Some result means, the None bit hardly conveys any information other than the obvious. Here is an official list of possible errors that can be raised by that particular call. By discarding the exception, it becomes impossible to diagnose what could have gone wrong.

Stringly-typed error handling

One could quickly point out that the snippet above can be amended so that data loss is minimized.

let readAllText (path : string) : Result<string, string> =
    try System.IO.File.ReadAllText path |> Ok
    with e -> Error e.Message

This alteration still makes error handling awkward and unsafe

match readAllText "foo.txt" with
| Error e when e.Contains "Could not find file" -> // ...

particularly when compared to how the idiomatic approach works

try File.ReadAllText path
with :? FileNotFoundException -> // ...

Interop issues

It is often easy to forget that Result values are plain objects, with no particular bearing when used in the context of other frameworks/libraries. The following example illustrates an innocent mistake when working with TPL:

let task =
    Task.StartNew(fun () ->
        if Random.Next() % 2 = 0
        then Ok ()
        else Error ())

if task.IsFaulted then printfn "Task failed"
else printfn "Task succeeded"

Or perhaps it could be mistakenly believed that F# Async somehow recognizes result types

let job1 = async { return Ok () }
let job2 = async { return Error () }

let! result =
    [job1 ; job2]
    |> Async.Parallel
    |> Ok

match result with
| Ok _ -> printfn "All jobs completed successfully"
| Error _ -> printfn "Some of the jobs failed"


It is not in the intentions of this article to argue that result types shouldn’t be used at all. I have written DSLs and interpreters using result types. MBrace uses result types internally. In most cases though, the benefit is only maximized by writing custom result types that model a particular domain. Rather than having a general-purpose error branch that encodes infinite classes of failures, assign each class of errors to an individual branch in that result type:

type MoneyWithdrawalResult =
    | Success of amount:decimal
    | InsufficientFunds of balance:decimal
    | CardExpired of DateTime
    | UndisclosedFailure

This approach constrains error classes in a finite domain, which also allows for more effective testing of our code.

That said, I strongly believe that using result types as a general-purpose error handling mechanism for F# applications should be considered harmful. Exceptions should remain the dominant mechanism for error propagation when programming in the large. The F# language has been designed with exceptions in mind, and has achieved that goal very effectively.


35 thoughts on “You’re better off using Exceptions

  1. Thanks for the interesting article.

    What do you think about using an Exception list as your Error type to mitigate some of these problems? Then you could have a helper to throw and catch an exception to retain stack traces. This would also make it easy to capture existing .NET exceptions (such as in your ReadAllText example) without losing any information. It also discourages stringly-typed exception handling compared with using string(s) as the Error type. All without losing the ability to capture multiple errors without stopping on the first one.

    1. Most of the times (when implementing things like interpreters) I’m using Choice<T,exn> as you suggest. This is indeed better than having stringly typed error messages. The main downside of this approach (as a general purpose error-management tools) is that you lose stacktraces.

      1. You could capture the stack trace if you throw and catch the exception before storing it as an Error (with a helper). This is what I was suggesting in my comment. Maybe that’s a bit much though!

  2. About stack traces, we use `| Error of exn`. It’s also fully composable compared to `rich` error types.

    About domain specific result types, I disagree that they are better than a generic type, because they are not composable (I mean no `bind`, `map`, etc., you have to write success / error propagation by hand for each result types combination).

    1. I agree that `| Error of exn` is better than just strings. However that approach still doesn’t account for 1) lost stacktraces and 2) the abstraction leak of Result-based functions also throwing.

      I cannot stress how bad and counterproductive that second issue can be. There you are spending time and effort writing ornate error handling flows that essentially replicate hard-coded exception semantics everywhere, yet *still* exceptions are capable of bubbling through. This can only be addressed by using a proper `Either` computation expression that takes care to trap exceptions from the underlying code, however in that case you’re just left with a pale imitation of regular exception semantics.

      The fact that custom error types are non-composable is precisely the point. They serve their purpose only as long as they remain within the boundaries of their domain.

  3. your `Customer` example is a bit sad because you should never use `.Value` in F# in the first place (I sometimes wish it would be hidden just like `.Item1` on tuples)

    Having said that: pure functions should almost never throw an exception and then it’s just a matter of annotating partial functions with Either, Maybe, Option, Choice, … whatever

    If your functions uses sideeffects anyway exceptions are fine IMO …

    1. All the examples used are adaptations of real issues I have seen in production code. While I would not write this, a less experienced colleague could easily have done it, and often small bugs like that can slip through code reviews. This is not an issue of experience though. Exceptions can slip through, even in pure code written by the most experienced hands.

      A less trivial example would be `let average (xs : float list) = List.average xs`. Is this throwing?

      1. Types have eliminated null ref exceptions and better typing here can eliminate the exceptions in the customer function.

        The function shouldn’t take a list it should take a list1 (or NonEmptyList). The .Value is a very junior error and should easily be caught in unit/property testing. Functions like .Value, Option.get List.average should be configured to lint out a warning.

        If you do this you get to more elm like code that can’t throw runtime exceptions. All external unsafe code and data needs to be handled and parsed at the door.

        Its a much easier world to work in. I’m not buying the its leaky so we should give up and use exceptions argument.

      2. Well perhaps we could configure the linter to warn on integer division as well then? What about

        Also, contrary to your assertion, null ref exceptions can happen inside otherwise pure functions. This happens because types not allowed to be null by the F# compiler can take the null representation at runtime. This is commonly experienced when things like web frameworks are passing values to our code:

        open Newtonsoft.Json
        type Foo = { Values : int list }
        let foo = JsonConvert.DeserializeObject<Foo>("{ }")
        List.length foo.Values

      3. Yes I do check for nan etc across all code in the unit testing. Quite easy to do with a conditional flag and open Checked. Essential in quant code. problems will be revealed in the property based tests.

        You need to eliminate nulls at the door. Make sure strings aren’t null etc. You wouldn’t use exceptions parsing a data feed would you?

      4. AFAIK the Checked module replaces overflows with overflow exceptions. Unit test error detection mechanisms use exceptions, even if property tests. Integer arithmetic doesn’t incude NaN.

        > You need to eliminate nulls at the door. Make sure strings aren’t null etc.
        Theoretically I do. Sometimes I forget.

        > You wouldn’t use exceptions parsing a data feed would you?
        Depends on the requirement.

      5. Yes so property based testing can root out problems even down to float nan issues.

        If you forget to parse data correctly then what are you doing with in the exception handling?

        I don’t understand what can be done apart from testing and fixing exceptions and failing fast.

      6. I think we’re on the same page really. We must accept that our code can raise exceptions and fail fast if it actually does. This is not advocating that we should be throwing more, but rather avoid preemptively catching anything that could potentially throw.

  4. PS: Exceptions in .net are *not* a efficient way of doing control-flow at all, which is yet another reason why you should not use them …

    Looking at Ocaml: exceptions there are cheap and used as control-flow all the time

  5. It’s worth noting that you can use the ExceptionDispatchInfo class to grab the stack trace for an exception at the point it is caught, if you’re using the Error of exn method. There’s a throw member on this class that is indistinguishable from the original exception, which allows you to have the original error but handle it elsewhere/using combinators.

    1. Sure, but it’s passing the exception through a wormhole. It threw in one context, passed around in another, then re-thrown somewhere else. Whatever happened in the middle is lost as far as the stacktrace is concerned.

  6. While I agree with many of the points in this article, I disagree with both the title and parts of the conclusion.

    I agree that there seems to be little value in trying to catch all exceptions only to repackage them as discriminated unions. As you write, there’s an immeasurable number of potential exception types, and if you aren’t going to handle them anyway, then where’s the value in trapping those exceptions?

    Where I disagree is that I find some of the arguments lacking. For instance, the argument under the ‘Boilerplate’ heading can be addressed by using a computation expression. I’ve written several applications (REST APIs) in F# using Result/Either, and I always use computation expressions. I agree that it would be cumbersome without that syntactic sugar, but with computation expressions, it’s straightforward.

    You also don’t have to resort to ‘stringly typed’ error handling. The entire point of bifunctor-based error handling is that you can use sum types to strongly type various error cases.

    Again, I agree with your points about trying to catch all exceptions and turn them into error values. If you’re not going to deal with them anyway, then what’s the point?

    On the other hand, for all the error cases that you do care about, I find Either types essential for modelling, and that applies to entire applications. I don’t think it’s something that has to be confined to domain models.

    For example, when I write REST APIs, I like to be able to distinguish between validation errors, domain model errors, and perhaps a few other groups of errors. The reason is that for a validation error, I want to return 400 (Bad Request), for certain domain model errors, I want to return 403 (Forbidden), and so on.

    Even though I do that, I don’t convert all exceptions to error values. If the final response is going to be 500 (Internal Server Error) anyway, the web framework is already going to return that for all unhandled exceptions, so I simply let that happen.

    Still, I find it indispensable to get the help of the type system, so that I know which error types I have to explicitly address.

    1. An Either computation expression -if implemented properly- does not suffer from some of the unsoundness described in the article. It hardly avoids some of the other pitfalls though, plus it carries awful performance.

      Perhaps I didn’t make this abundantly explicit, but my intention in writing this article was to attack a very particular style that is popular in the community. This is promoting strings (or lists of strings) as error types without the use of computation expressions, as a panacea for error handling.

      A common criticism I have been reading over the past couple of days is that my examples are strawmans, and surely no sensible dev would be writing code like this. However, I assure you that all examples are inspired by real code, both proprietary and OSS. It’s causing real disruption.

      The burning issue in my mind is that we (the community) are signaling that exceptions are the devil and Result<‘T> is our saviour. This is all cute but devs/shops that don’t know any better do take the bait. After that it’s Result everywhere, boilerplate everywhere, unsoundness everywhere.

      I’m aware of the validation application for web apps and was doing it like you describe until recently. Then I switched to HttpResponseException and it greatly simplified my code. It even lets me define endpoints with strongly typed return values for swagger.

      1. While I agree in principle, there is an appeal to mechanical sympathy when using that particular construct. It should go without saying that I only raise such exceptions on the controller level and not in underlying implementations.

  7. I like to define my domain message types as records instead of unions, but with a union as one of the members. This makes a pattern like this clean and clear:

    type ErrorKind =
    | … many cases can be bare tags with no interior data
    | … some cases can include extra data to help with error interpretation or presentation
    type DomainError = {kind: ErrorKind; msg: string; st: StackTrace option}

    The functions that produce these domain messages can optionally collect stack trace objects (such as when the application is running in debug mode) but not do so in production for speed. If you need to gobble up an exception at a domain boundary, you can grab its stack trace, or store the exception itself in the kind field.

    You don’t need to gobble every exception into Result, though, as exceptions are a great way of signaling an unrecoverable/programmer/assertion failure and those should bubble up to a single place that handles them.

  8. I think it’s important to consider what type of errors we are talking about: a programming bug or some invalid (system) state or data. Bugs should be propagated by exceptions and not be caught at all or only in outer frames. Throwing an exception in case of e.g. invalid user data seems not to be an appropiate thing. Compare to the bool Try* Api style introduced with some types in the Bcl. The “leaky” abstraction is not so leaky imho. “Choice” for errors in the domain, exceptions for bugs or really disastrous conditions like for instance OutOfMemory.

    1. I acknowledge the separation you’re trying to make, and I really don’t mind as long as it is being made consciously. It is the mindless adherence to one or the other approach that I object to.

      Personally, I would say it depends. While I do err on the side of not throwing myself, in some cases it may be justified. Argument validation is often a good candidate. Multi-threaded workflows where exceptions carry cancellation semantics could be another. In some cases you just want to give up and kill your process with appropriate debug information.

      1. I wondered if you failed to recognize what railway oriented programming is good for, and what it definitely isn’t good for. Or maybe you do I am thinking now, and you are just beating a dead horse – or not. Is there really anybody out there handling all exceptions with result types? I find that hard to believe.

        Railway oriented programming seems highly effective to me, but of course you should only turn specific expected exceptions into a result type. That means exceptions that naturally falls into the result domain of the function. Exceptions that don’t, should be passed out and caught elsewhere. Catch what you can expect, but don’t catch everything – that’s the general rule to follow in any language, and also in F# even if you use result types and railway oriented programming.

        In one place you write “This eventually delivers an unpleasant surprise to consumers of the function, since the type signature communicates an exception-free implementation.” First, programming errors that result in exceptions can occur in any function whether we use result types or not, and to expect a function to be immune to this based on a type signature doesn’t make sense to me. Second, when using result types, as already mentioned, we normally wouldn’t (or shouldn’t) handle all exceptions anyway. If you do, then it’s a mistake by a purist’s point of view, but no worse a mistake than what C# programmers do all the time when using catch all.

      2. Is there really anybody out there handling all exceptions with result types? I find that hard to believe.


  9. The problems in this article are mostly stemming from the misuse of the Result type. It’s the classic tale of discovering a pattern and trying to apply it everywhere. And as you’ve noticed (and I also learned the hard way), trying to use Result everywhere turns out to be pretty hard to deal with.

    Now I primarily use the Result type in 2 places. At the edge of the system where IO is called. Most of the time IO libraries throw exceptions, and the Error value I use is simply a union type that labels what was happening when the exception was thrown. (e.g. `| RequestDeserializationFailed of exn`). The other place is typically validation. Validation error cases can be represented by strings if desired, because they are not fairly represented by exceptions. (When using exceptions for validation errors, usually ex.Message is the only important part of the exception. Do you see how the Result usage you described is a reaction to this?)

    When I write code for an IO library, then I tend to NOT use Result for that. For example with SQL, I have a `writeBatch` function that takes a number of statements (query and parameters) and combines them into one large parameterized statement. Being that the library I’m interacting with to build this statement is mostly imperative/mutable, I don’t use Result. I let it throw exceptions. It doesn’t make sense to have to deal with Result at every step only to put it right back into exception-throwing code of the IO library. And it’s not cost-effective to wrap every interaction with the library to Result. Instead, use it as-is to make a courser-grained, exception-throwing operation. Then the boundary layer can convert exceptions from the higher level operation to a descriptive Error case (e.g. `| SaveFailed of exn`). The exception itself describes specifically what went wrong.

    And for interop, my boundary layer converts the Error case to whatever it needs to be. In an HTTP API, that would be a (non-200) HTTP response. This is great because I have a holistic view of what kinds of errors I’m generating and what they map to as a response. Also, one place to log errors.

    Anyway, I have code in production using (Async)Result in the ways I described above. I have found it to be a great help rather than a hindrance. Although I resisted for a while, I also do now use “standard” operators like `>>=` for bind to reduce boilerplate. Apparently I’m an odd duck regarding computation expressions… I don’t use them for (Async)Result.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s