United Kingdom: +44 (0)208 088 8978

Mastering Results: How to handle domain errors elegantly

Dealing with errors in an application can seem daunting: there's so much that can go wrong! In this blog post Jaz has a look at the Result type, and how we can use it for clear and exhaustive handling of domain errors

We're hiring Software Developers

Click here to find out more

Today's focus is how to work with F#'s Result, which is a type I personally reach for very often.

Once you are familiar with Result it it is a joy to use, but sometimes you find yourself in a situation where you are unsure how to get from where you are to where you want to be.

Results, Choices, Exceptions

The Result type is a bit opinionated. In contrast to the Choice type's cases which are numbered, or Haskell's Either which has left and right, the naming scheme of Ok and Error makes it suited to processes where there is an obvious "happy path" and "sad path". Sometimes a sad path can be confused with an exceptional path. In my opinion they are separate!

  • The happy path, represented by Ok, is what you are hoping will happen.
  • The sad path, represented by Error, is what you were hoping would not happen - but it is not unexpected.
  • The exceptional path, represented by a raised exception is typically something which is outside the scope of the domain we are operating in - such as an IP being unreachable or database being down (both are probably exceptional, unless those are your domain!).

Sometimes APIs - especially those not created using F# - are created in a way where all sad paths are exceptions (!), and in such cases it is totally fine to manually convert to a Result.Error instead.

Here's a short - slightly contrived - example which does some local validation on some data before attempting to store it in a database:

let validateData string: Result<Data, DataError list> =
    ...

let saveData database string: unit =
    // throws PrimaryKeyViolationException if the data is already present
    // throw ConnectionException if a connection to the DB cannot be made
    ...

let saveInput rawInput =
    match validateData rawInput with
    | Ok validatedData ->
        try
            Ok (saveData database validatedData)
        with
        | :? PrimaryKeyViolationException -> Error UserAlreadyExists
        | :? ConnectionException -> reraise ()
    | Error e -> Error e

Here, we use try ... with to catch the PrimaryKeyViolationException and map it into our error domain, and reraise the connection exception to keep it as an exception.

Working with multiple results

Let's take a look at a different example, this one contains multiple results.

Note: In this example I've opted to use the result computation expression for syntactical clarity. This is functionally equivalent to nested matching into Ok branches, or chaining Result.bind between each let! expressions. Check out Isaac's post on computation expressions to learn a bit more, including how they can be used to create domain specific languages.

let validateEmail: Result<Email, EmailError> = ...
let validatePassword: Result<Password, PasswordError> = ...
type User = { Email: Email; Password: Password }

let validateUser form =
    result {
        let! email = validateEmail form.Email
        let! password = validatePassword form.Password
        return { Email = email; Password = password }
    }

There are some things to note:

1) The error type of the validate functions do not match, this code will not compile!
1) If there is an error it will return immediately. So if the email and the password are both invalid, the user won't know about any password errors until the email is fixed.

Let's address these points in order.

Uniting error types

When first encountered, figuring out how to join up different types may not be obvious. Some developers will use an error type of string. That's a shame as, in contrast to discriminated unions, strings are not very well suited to pattern matching, so by converting them at this point we lose a lot of flexibility.

A better way would be to create a type which unites the disparate error types.

The perfect type for uniting types is... a union type. (surprise!) Let's give it a go, all we need to do is:

  1. Create a DU which has a case for each type of error we need to unite.
  2. Pipe different results into Result.mapError and map to the matching case.
[<RequireQualifiedAccess>]
type UserValidationError =
    | Email of EmailError
    | Password of PasswordError

let validateUser form =
    result {
        let! email =
            validateEmail form.Email
            |> Result.mapError UserValidationError.Email

        let! password =
            validatePassword form.Password
            |> Result.mapError UserValidationError.Password

        return { Email = email; Password = password }
    }

Now this code will compile, and we can pattern match out any specific errors as needed. More on that later.

Returning more than one error

Now onto the second point - what about when we want to see more errors than just the first one we encounter? In this case we want to figure out ALL the errors, and then return.

We now need to mix in some kind of collection since we will be returning potentially multiple errors.

let validateUser form =
    let email =
        validateEmail form.Email
        |> Result.mapError UserValidationError.Email

    let password =
        validatePassword form.Password
        |> Result.mapError UserValidationError.Password

    match email, password with
    | Ok email, Ok password -> Ok { Email = email; Password = password }
    | Error email, Ok _ -> Error [ email ]
    | Ok _, Error password -> Error [ password ]
    | Error email, Error password -> Error [ email; password ]

A bunch of repetitive and error-prone boilerplate, how horrible!

Also, notice that we have removed the result CE, and switched let! to simple let bindings, that's why we have to do all the manual type unwrapping in the match statement at the end. This is to avoid the default behaviour of result which would exit as soon as any error is encountered.

Fortunately, we can lean on the FsToolkit.ErrorHandling library to make this a less painful experience.

Let's try out the validation computation expression, with the and! keyword. When using and! we get back all the errors if there are any, not the first one.

let validateUser form =
    validation {
        let! email =
            validateEmail form.Email
            |> Result.mapError UserValidationError.Email

        and! password =
            validatePassword form.Password
            |> Result.mapError UserValidationError.Password

        return { Email = email; Password = password }
    }

Well, that's a lot more manageable.

This block of code returns a Validation<User, UserValidationError>, which is a type alias for Result<User, UserValidationError list> - the same as the return type of the previous example, but a lot easier to write.

Processing error lists

We've looked at how to get errors unified, and now we're ending up with lists of errors. The next question is usually something like this: "how do we extract the base error types from the unified list?"

I tend to write little helper functions to match out each case of the combined DU. We can then feed those into List.choose to get out a list of the errors we care about.

module UserValidationError =
    let tryEmail =
        function 
        | UserValidationError.Email x -> Some x
        | _ -> None

    let tryPassword =
        function 
        | UserValidationError.Password x -> Some x
        | _ -> None

match validateUser form with
| Ok user ->
    // Do something with the validated User
| Error errorList ->
    // Tell the user about the validation errors somehow!
    let emailErrors = errorList |> List.choose UserValidationError.tryEmail
    let passwordErrors = errorList |> List.choose UserValidationError.tryPassword

Conclusion

Hopefully you have found this review of some of the slightly more advanced result techniques useful.

For more on this style of programming by creating a pipeline of results, check out this F# for Fun and Profit series: Railway-oriented programming which I would highly recommend. Don't skip the follow up post which talks about when this style is not suitable!

For more on the validation type from FsToolkit, check Ryan's post Validation with F# 5 and FSToolkit.

Finally, I would be remiss not to point out that not everyone agrees on how to use results! For some different views on F#s Result type, check out these posts: