F# Error Handling with 'Result'
Tags: fsharp, dotnet, ddd, errors
Published: 2020-01-03 15:29:47

Handling errors with `Result` is a great way to model errors in your domain and force you to consider all the error cases[^1]. But if you're used to using exceptions for this sort of thing, it can be tricky to figure out the best way to glue all your `Result`-returning functions together.

To illustrate some different approaches, we'll go through a real-life example of making an HTTP request, checking the status code, then attempting to decode the response body.

Here are the types we'll be working with to make our request:

```fsharp
type Response = {
    Body : string
    StatusCode : int
}

type RequestError =
| NetworkError

let makeHttpRequest (url : string) : Async<Result<Response, RequestError>> = ...
```

This can return an `Error` in case there's a network issue. And even if the request does succeed, we need to consider the possibility of a non-`200` status code.

Here's our `User` type and a function to decode a string. If it fails to parse, it will return an `Error` with a message.

```fsharp
type User = {
    Id : string
    Name : string
}

let decodeUser (body : string) : Result<User, string> = ...
```

I deliberately left out the contents of `makeHttpRequest` and `decodeUser`; we're only interested in the type signatures here.

The most interesting code we'll look at is the `getUser` function below. It ties everything together and allows us to explore all the different ways to combine the error-handling logic.

## Pattern Matching

We'll start with a pretty straightforward variant:

```fsharp
type GetUserError =
| NetworkError
| Non200Response
| ParseError of string

let getUser (url : string) : Async<Result<User, GetUserError>> =
    async {
        let! result = makeHttpRequest url

        return
            match result with
            | Error _ -> Error NetworkError
            | Ok response ->
                match response.StatusCode with
                | 200 ->
                    match decodeUser response.Body with
                    | Error err -> Error (ParseError err)
                    | Ok user -> Ok user
                | _ ->
                    Error Non200Response
    }
```

We use an `async` computation expression, then pattern match to handle all the errors. This is simple and pretty easy to write, though there's a lot of nesting, and it's needlessly verbose in a few cases. All the plumbing is out there in the open, for better or worse.

## Towards a Pipeline

We can eliminate some of the plumbing by embracing `Result.bind` and `Result.mapError`:

```fsharp
let getUser (url : string) : Async<Result<User, GetUserError>> =
    async {
        let! result = makeHttpRequest url

        return
            result
            |> Result.mapError (fun _ -> NetworkError)
            |> Result.bind (fun response ->
                match response.StatusCode with
                | 200 ->
                    response.Body
                    |> decodeUser
                    |> Result.mapError ParseError
                | _ ->
                    Error Non200Response
            )
    }
```

`Result.bind` chains actions together in the success case. `Result.mapError` takes any failures that occur along the way and maps them to a common `GetUserError` type.

## Embrace the Pipeline

So far we have a bit of a hybrid, using a computation expression for `async`, and function pipelines for dealing with `Result`. That's fine, but let's see how it feels to go all-in.

Here we'll use the `AsyncResult` module from [`FsToolkit.ErrorHandling`](https://github.com/demystifyfp/FsToolkit.ErrorHandling) which allows us to drop the `async` computation expression:

```fsharp
let getUser (url : string) : Async<Result<User, GetUserError>> =
    makeHttpRequest url
    |> AsyncResult.mapError (fun _ -> NetworkError)
    |> AsyncResult.bind (fun response ->
        match response.StatusCode with
        | 200 ->
            response.Body
            |> decodeUser
            |> Result.mapError ParseError
            |> Async.singleton
        | _ ->
            AsyncResult.returnError Non200Response
    )
```

Not too much is different here. We use `mapError` and `bind` from `AsyncResult` instead of `Result`. We also have to use `Async.singleton` to lift our decode `Result` into `Async<Result>`.

Nesting is pretty minimal, and it's pretty easy to see how our data flows from request to status code checking to decoding the response.

## Computation Expressions, My Old Friend

All right, let's see how things would look if we instead go the other way and lean harder into computation expressions. We'll use the `asyncResult` builder from `FsToolkit.ErrorHandling`:

```fsharp
let getUser (url : string) : Async<Result<User, GetUserError>> =
    asyncResult {
        let! response =
            makeHttpRequest url
            |> AsyncResult.mapError (fun _ -> NetworkError)
        
        do!
            response.StatusCode = 200
            |> Result.requireTrue Non200Response

        return!
            response.Body
            |> decodeUser
            |> Result.mapError ParseError
    }
```

Nesting is eliminated completely — computation expressions are good at that. We traded in our pattern matching on `response.StatusCode` for an interesting helper, `Result.requireTrue`. Part of me prefers the explicit pattern matching, but `Result.requireTrue` seems to fit the style better here.

Of all the variants, this one seems to do the best job of separating the distinct phases of making the request, checking the status code, and decoding the response. Unfortunately, it also seems to give the worst compiler errors, e.g. `No overloads match for method 'Bind'` if you forget to call `AsyncResult.mapError` on your response `Result`. Computation expressions are awesome, but you almost need to understand how they work under the hood to be able to understand errors like this.

## Conclusion

Which of these approaches is the best? It's not clear to me. All of the variants seem to come with their own set of trade-offs. Some are easier to write, but harder to read (and vice versa). Some are friendlier to beginners, and others use more advanced language features.

But that's kind of the point. There's no one best way to do it. It depends on the context, and you need to play around with it to find something that feels right. In the end, it may come down to personal or team preference.

I encourage you to get comfortable with all of the approaches I outlined. Start simple, and if you're not happy with it, then try a different approach and compare.

[^1]: Scott Wlaschin has written a lot on this topic. He coined the term [Railway Oriented Programming](https://fsharpforfunandprofit.com/posts/recipe-part2/) to describe this style of monadic error handling. More recently, he cautioned about taking it too far. I like his [distinction between domain errors, infrastructure errors, and panics](https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/#when-should-you-use-result) as a guideline for whether you should use `Result` or exceptions.