How to prevent application-specific error cases from infecting your domain models.

Functional error-handling is often done with the Either monad. If all is good, the right case is returned, but if things go wrong, you'll want to return a value that indicates the error. In an application, you'll often need to be able to distinguish between different kinds of errors.

From application errors to HTTP responses

When an application encounters an error, it should respond appropriately. A GUI-based application should inform the user about the error, a batch job should log it, and a REST API should return the appropriate HTTP status code.

Regular readers of this blog will know that I write many RESTful APIs in F#, using ASP.NET Web API. Since I like to write functional F#, but ASP.NET Web API is an object-oriented framework, I prefer to escape the object-oriented framework as soon as possible. (In general, it makes good architectural sense to write most of your code as framework-independent as possible.)

In my Test-Driven Development with F# Pluralsight course (a free, condensed version is also available), I demonstrate how to handle various error cases in a Controller class:

type ReservationsController (imp) =
    inherit ApiController ()
 
    member this.Post (dtr : ReservationDtr) : IHttpActionResult =
        match imp dtr with
        | Failure (ValidationError msg) -> this.BadRequest msg :> _
        | Failure CapacityExceeded ->
            this.StatusCode HttpStatusCode.Forbidden :> _
        | Success () -> this.Ok () :> _

The injected imp function is a complete, composed, vertical feature implementation that performs both input validation, business logic, and data access. If input validation fails, it'll return Failure (ValidationError msg), and that value is translated to a 400 Bad Request response. Likewise, if the business logic returns Failure CapacityExceeded, the response becomes 403 Forbidden, and a success is returned as 200 OK.

Both ValidationError and CapacityExceeded are cases of an Error type. This is only a simple example, so these are the only cases defined by that type:

type Error =
| ValidationError of stringCapacityExceeded

This seems reasonable, but there's a problem.

Error infection

In F#, a function can't use a type unless that type is already defined. This is a problem because the Error type defined above mixes different concerns. If you seek to make illegal states unrepresentable, it follows that validation is not a concern in your domain model. Validation is still important at the boundary of an application, so you can't just ignore it. The ValidationError case relates to the application boundary, while CapacityExceeded relates to the domain model.

Still, when implementing your domain model, you may want to return a CapacityExceeded value from time to time:

// int -> int -> Reservation -> Result<Reservation,Error>
let checkCapacity capacity reservedSeats reservation =
    if capacity < reservation.Quantity + reservedSeats
    then Failure CapacityExceeded
    else Success reservation

Notice how the return type of this function is Result<Reservation,Error>. In order to be able to implement your domain model, you've now pulled in the Error type, which also defines the ValidationError case. Your domain model is now polluted by an application boundary concern.

I think many developers would consider this trivial, but in my experience, failure to manage dependencies is the dominant reason for code rot. It makes the code less general, and less reusable, because it's now coupled to something that may not fit into a different context.

Particularly, the situation in the example looks like this:

Dependency diagram

Boundary and data access modules depend on the domain model, as they should, but everything depends on the Error type. This is wrong. Modules or libraries should be able to define their own error types.

The Error type belongs in the Composition Root, but it's impossible to put it there because F# prevents circular dependencies (a treasured language feature).

Fortunately, the fix is straightforward.

Mapped Either values

A domain model should be self-contained. As Robert C. Martin puts it in APPP:

Abstractions should not depend upon details. Details should depend upon abstractions.
Your domain model is an abstraction of the real world (that's why it's called a model), and is the reason you're developing a piece of software in the first place. So start with the domain model:

type BookingError = CapacityExceeded
 
// int -> int -> Reservation -> Result<Reservation,BookingError>
let checkCapacity capacity reservedSeats reservation =
    if capacity < reservation.Quantity + reservedSeats
    then Failure CapacityExceeded
    else Success reservation

In this example, there's only a single type of domain error (CapacityExceeded), but that's mostly because this is an example. Real production code could define a domain error union with several cases. The crux of the matter is that BookingError isn't infected with irrelevant implementation details like validation error types.

You're still going to need an exhaustive discriminated union to model all possible error cases for your particular application, but that type belongs in the Composition Root. Accordingly, you also need a way to return validation errors in your validation module. Often, a string is all you need:

// ReservationDtr -> Result<Reservation,string>
let validateReservation (dtr : ReservationDtr) =
    match dtr.Date |> DateTimeOffset.TryParse with
    | (true, date) -> Success {
        Reservation.Date = date
        Name = dtr.Name
        Email = dtr.Email
        Quantity = dtr.Quantity }
    | _ -> Failure "Invalid date."

The validateReservation function returns a Reservation value when validation succeeds, and a simple string with an error message if it fails.

You could, conceivably, return string values for errors from many different places in your code, so you're going to map them into an appropriate error case that makes sense in your application.

In this particular example, the Controller shown above should still look like this:

type Error =
| ValidationError of stringDomainError
 
type ReservationsController (imp) =
    inherit ApiController ()
 
    member this.Post (dtr : ReservationDtr) : IHttpActionResult =
        match imp dtr with
        | Failure (ValidationError msg) -> this.BadRequest msg :> _
        | Failure DomainError -> this.StatusCode HttpStatusCode.Forbidden :> _
        | Success () -> this.Ok () :> _

Notice how similar this is to the initial example. The important difference, however, is that Error is defined in the same module that also implements ReservationsController. This is part of the composition of the specific application.

In order to make that work, you're going to need to map from one failure type to another. This is trivial to do with an extra function belonging to your Result (or Either) module:

// ('a -> 'b) -> Result<'c,'a> -> Result<'c,'b>
let mapFailure f x =
    match x with
    | Success succ -> Success succ
    | Failure fail -> Failure (f fail)

This function takes any Result value and maps the failure case instead of the success case. It enables you to transform e.g. a BookingError into a DomainError:

let imp candidate = either {
    let! r = validateReservation candidate |> mapFailure ValidationError
    let  i = SqlGateway.getReservedSeats connectionString r.Date
    let! r = checkCapacity 10 i r |> mapFailure (fun _ -> DomainError)
    return SqlGateway.saveReservation connectionString r }

This composition is a variation of the composition I've previously published. The only difference is that the error cases are now mapped into the application-specific Error type.

Conclusion

Errors can occur in diverse places in your code base: when validating input, when making business decisions, when writing to, or reading from, databases, and so on.

When you use the Either monad for error handling, in a strongly typed language like F#, you'll need to define a discriminated union that models all the error cases you care about in the specific application. You can map module-specific error types into such a comprehensive error type using a function like mapFailure. In Haskell, it would be the first function of the Bifunctor typeclass, so this is a well-known function.



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or Google Plus, or somewhere else with a permalink. Ping me with the link, and I may add it as a comment.

Published

Tuesday, 03 January 2017 12:26:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!