As a user, I want to change the name and email in the system.
To implement this simple user history, we must receive a request, validate, update an existing record in the database, send a confirmation email to the user and return the answer to the browser. The code will look about the same in C #:
string ExecuteUseCase() { var request = receiveRequest(); validateRequest(request); canonicalizeEmail(request); db.updateDbFromRequest(request); smtpServer.sendEmail(request.Email); return "Success"; }
and F #:
let executeUseCase = receiveRequest >> validateRequest >> canonicalizeEmail >> updateDbFromRequest >> sendEmail >> returnMessage
Deviating from a happy journey

')
Complete the story:
As a user, I want to change the name and email in the system
And see the error message if something goes wrong.
What can go wrong?

- Name may be empty, and email - not correct
- user with such id may not be found in the database
- SMTP server may not respond when sending a confirmation email
- ...
Add error handling code
string ExecuteUseCase() { var request = receiveRequest(); var isValidated = validateRequest(request); if (!isValidated) { return "Request is not valid" } canonicalizeEmail(request); try { var result = db.updateDbFromRequest(request); if (!result) { return "Customer record not found" } } catch { return "DB error: Customer record not updated" } if (!smtpServer.sendEmail(request.Email)) { log.Error "Customer email not sent" } return "OK"; }
Suddenly, instead of 6, we got 18 lines of code with branches and more nesting, which greatly worsened readability. What will be the functional equivalent of this code? It looks exactly the same, but now it has error handling. You may not believe me, but when we get to the end, you will see that this is true.
Request-response architecture in imperative style

We have a request, an answer. Data is passed along the chain from one method to another. If an error occurs we simply use the early return.
Functional-style request-response architecture

On the "happy path" everything is absolutely the same. We use composition of functions to transfer and process the message in a chain. But if something goes wrong, we must pass an error message as the return value from the function. So, we have two problems:
- How to ignore the remaining functions in case of an error?
- How to return four values ​​instead of one (one return value for each type of error)?
How can a function return more than one value?
In functional PL
types, union types are widespread. With their help, you can simulate several possible states within the same type. The function has one return value, but now it takes one of four possible values: success or type of error. It remains only to summarize the data approach. We declare the
Result type consisting of two possible values ​​of
Success
and
Failure
and add a generic argument with the data.
type Result<'TEntity> = | Success of 'TEntity | Failure of string
Functional design

- Each use case is implemented using one function.
- Functions return a join from
Success
and Failure
- The function for processing a use case is created using a composition of smaller functions, each of which corresponds to one data conversion step.
- Errors at each step will be combined to return a single value.
How to handle errors in a functional style?

If you have a very smart friend, well versed in FP, you may have a dialogue like this:
- I would like to use composition of functions, but I lack a convenient way to handle errors.
- Oh, it's simple. You need a monad
- It sounds hard. What is a monad?
- A monad is just a monoid in the category of endofunctors.
- ???
- What is the problem?
- I don't know what an endofunctor is.
- It's simple. A functor is a homomorphism between categories. And an endofunctor is just a functor that maps a category onto itself.
- Well, of course! Now everything became clear ...
Next comes the original untranslatable wordplay, based on Maybe
(maybe) and Either
(or one or the other). Maybe
and Either
are also monad names. If you like English humor and you also consider the terminology of OP too “academic”, be sure to check out the original report .
Connection with the Either monad and the composition Kleisli

Any Haskell fan will notice that the approach I described is the
Either
monad, a specialized type of error list for the “Left” (
Left
) case. In Haskell, we could write this:
type Result ab = Either [a] (b,[a])
Of course, I'm not trying to impersonate the inventor of this approach, although I claim to be a stupid analogy with the railroad. So why didn’t I use standard Haskell terminology? First, this is not another monad guide. Instead, the main focus is shifted to solving a specific error handling problem. Most people who start learning F # are not familiar with monads, so I prefer the less frightening, more visual and intuitive approach for many.
Secondly, I am convinced that the approach from the particular to the general is more effective: it is much easier to climb to the next level of abstraction, when you understand the current one well. I would be wrong if I called my “double-track” approach a monad. Monads are more complicated and I don’t want to go into the
monadic laws in this material.
Thirdly,
Either
is too general a concept. I would like to submit a recipe, not a tool. The recipe for making bread, which says "just use flour and oven" is not very useful. It’s also absolutely useless to have a “just use
bind
and
Either
” error handling guide. Therefore, I propose an integrated approach that includes a whole set of techniques:
- List of specialized error types, instead of just
Either String a
bind (>>=)
for the composition of monadic functions in the pipeline- Cleisley composition (
>=>
) for composition of monadic functions map
and fmap
for integrating non-monadic functions into the pipelinetee
function to integrate functions returning a unit
(analog void
in F #)- error code mapping
&&&
for combining monadic functions in parallel processing (for example, for validation)- Benefits of Using Error Codes in Domain Driven Design (DDD)
- obvious extensions for logging, domain events, compensatory transactions, and more
I hope you enjoy it more than just “use the Either monad.”
Railroad analogy

I like to present the function as a train track and a transformation tunnel. If we have two functions, one converting apples into bananas (
apple → banana
) and the other bananas into cherries (
banana → cherry
), combining them we get the functions of converting apples to cherries (
apple → cherry
). From the point of view of the programmer, there is no difference. This function is obtained using composition or written by hand, the main thing is its signature.
Fork
But we have a slightly different case: one value at the input and two possible ones at the output: one branch for successful completion and one for error. In the "railway" terminology, we need a fork.
Validate
and
UpdateDb
are such fork functions. We can combine them with each other. Add the
UpdateDb
function to
Validate
and
SendEmail
. I call it the “double track model.” Some people prefer to call this approach to error handling "monad Either", but I like my name more (if only because it does not have the word "monad").

Now there are “single track” and “double track” functions. Separately, both those and others are assembled, but they are not linked with each other. For this we need a small "adapter". In case of success, we call the function and pass the value to it, and in case of an error, we simply pass the error value further without changes. In the FP, this function is called
bind
.

bind

let bind switchFunction = fun twoTrackInput -> match twoTrackInput with | Success s -> switchFunction s | Failure f -> Failure f // ('a -> Result<'b>) -> Result<'a> -> Result<'b>
As you can see, this function is very simple: just a few lines of code. Pay attention to the function signature. Signatures are very important in OP. The first argument is the “adapter”, the second argument is the input value in the two-track model, and the output is also the value in the two-track model. If you see this signature with any other types: with a
list
,
asyn
,
feature
or
promise
, you’ll have the same
bind
. The function may be called differently, for example,
SelectMany
in
LINQ
, but the essence does not change.
Validation

For example, there are three validation rules. We can concatenate several validation rules using
bind
(to transform each one of them to a “two-track model”) and composition of functions. That's the whole secret of error handling.
let validateRequest = bind nameNotBlank >> bind name50 >> bind emailNotBlank
Now we have a “double track” function that accepts a request for input and returns a response. We can use it as a building block for other functions.
Often
bind
denoted by the
>>=
operator. He borrowed from Haskell. If you use
>>=
code will look like this:
let (>>=) twoTrackInput switchFunction = bind switchFunction twoTrackInput let validateRequest twoTrackInput = twoTrackInput >>= nameNotBlank >>= name50 >>= emailNotBlank
When using
bind
type checking works the same way as before. If you had composable functions, they will remain composable after using
bind
. If the functions were not composable, then
bind
will not make them so.
So, the base for error handling is as follows: we convert functions to a “two-track model” with
bind
and combine them with composition. We move along the green gauge until everything is fine or we turn to red in case of an error.
But that is not all. We will need to enter in this model
- single-track functions without errors
- deadlock functions
- functions throwing exceptions
- control functions
Single-track functions without errors

let canonicalizeEmail input = { input with email = input.email.Trim().ToLower() }
The
canonicalizeEmail
function is very simple. It cuts off extra spaces and converts email to lower case. It should not contain errors and exceptions (except NRE). This is just a string conversion.
The problem is that we learned how to compile using
bind
only double track functions. We will need another adapter. This adapter is called a
map
(
Select
in
LINQ
).
let map singleTrackFunction twoTrackInput = match twoTrackInput with | Success s -> Success (singleTrackFunction s) | Failure f -> Failure f // map : ('a -> 'b) -> Result<'a> -> Result<'b>
map
is a weaker function than
bind
, because you can create a
map
with
bind
, but not vice versa.

Dead-end functions

let updateDb request = // do something // return nothing at all
Dead-end functions are write operations in the spirit of fire & forget: you update the value in the database or write a file. They have no return value. They are also not bundled with double track functions. All we need to do is get an input value, perform a dead-end function, and pass the value further down the chain. By analogy with
bind
and
map
declare functions
tee
(sometimes called
tap
).
let tee deadEndFunction oneTrackInput = deadEndFunction oneTrackInput oneTrackInput

Functions that throw exceptions

You, probably, have already noticed that a certain “pattern” began to emerge. Especially functions working with I / O. The signatures of such methods lie because, besides successful completion, they can throw an exception, thus creating additional exit points. From the signature of this is not visible, you need to familiarize yourself with the documentation in order to know what exceptions this or that function throws out.
Exceptions are not suitable for this "double track" model. Let's process them: the
SendEmail
function looks safe, but it can throw an exception. Add another “adapter” and wrap all such functions in a try / catch block.
“ Do or do not, there is no try ” - even Yoda does not recommend using exceptions for control flow . Many interesting things about this topic in Adam Sitnik's Exceptional Exceptions report (in English).
Control functions

In such functions, you just need to implement additional logic, for example, logging only successful operations or errors, or both. Nothing complicated, we do by analogy with the previous cases.
Putting it all together

We combined the functions
Validate
,
Canonicalize
,
UpdateDb
and
SendEmail
. There was one problem. The browser does not understand the "double-track model." Now you need to go back to the "single-track" model. Add the
returnMessage
function. We return http code 200 and JSON success or
BadRequest
and a message in case of an error.
let executeUseCase = receiveRequest >> validateRequest >> updateDbFromRequest >> sendEmail >> returnMessage
So, I promised that the code without error handling will be identical to the error handling code. I admit, I cheated a little and declared new functions in a different namespace, wrapping functions to the left in
bind
.
Expanding the framework
- Considering possible errors in the design
- Parallelization
- Domain Events
Considering possible errors in the design
I want to emphasize that
error handling is part of the software requirements . We focus only on successful scenarios. It is necessary to level successful scenarios and rights errors.
let validateInput input = if input.name = "" then Failure "Name must not be blank" else if input.email = "" then Failure "Email must not be blank" else Success input // happy path type Result<'TEntity> = | Success of 'TEntity | Failure of string
Consider our validation function. We use strings for errors. This is a disgusting idea. We introduce special types for errors. In F #, the union type is usually used instead of enum. Declare the type ErrorMessage. Now, in the event of an error when a new error appears, we will have to add another option to the ErrorMessage. This may seem like a burden, but I think it is, on the contrary, good, because such code is self-documenting.
let validateInput input = if input.name = "" then Failure NameMustNotBeBlank else if input.email = "" then Failure EmailMustNotBeBlank else if (input.email doesn't match regex) then Failure EmailNotValid input.email else Success input // happy path type ErrorMessage = | NameMustNotBeBlank | EmailMustNotBeBlank | EmailNotValid of EmailAddress
Imagine that you are working with legacy code. You have a general idea of ​​how the system should work, but you do not know exactly what can go wrong. What if you had a file that describes all possible errors? And more importantly, it’s not just text, but code, so this information is relevant.
This approach is very similar to checked exceptions in Java. It is worth noting that they did not take off .
If you practice DDD, then you can build communication with business users based on this code. You will have to ask questions about how to handle this or that situation, which in turn will force you and business users to consider more use cases at the design stage.
After we replace strings with error types, we will have to modify the
retrunMessage
function to convert the types to strings.
let returnMessage result = match result with | Success _ -> "Success" | Failure err -> match err with | NameMustNotBeBlank -> "Name must not be blank" | EmailMustNotBeBlank -> "Email must not be blank" | EmailNotValid (EmailAddress email) -> sprintf "Email %s is not valid" email // database errors | UserIdNotValid (UserId id) -> sprintf "User id %i is not a valid user id" id | DbUserNotFoundError (UserId id) -> sprintf "User id %i was not found in the database" id | DbTimeout (_,TimeoutMs ms) -> sprintf "Could not connect to database within %i ms" ms | DbConcurrencyError -> sprintf "Another user has modified the record. Please resubmit" | DbAuthorizationError _ -> sprintf "You do not have permission to access the database" // SMTP errors | SmtpTimeout (_,TimeoutMs ms) -> sprintf "Could not connect to SMTP server within %i ms" ms | SmtpBadRecipient (EmailAddress email) -> sprintf "The email %s is not a valid recipient" email
The conversion logic may be context-sensitive. This greatly facilitates the task of internationalization: instead of searching for the lines scattered throughout the codebase, you only need to make a change in one function, right before transferring control to the UI layer. Summarizing, we can say that this approach has the following advantages:
- documentation for all cases in which something went wrong
- Tipo-safe, can not become obsolete
- reveals hidden requirements for sistemie
- simplifies unit testing
- simplifies internationalization
Parallelization

In the example with validation, the sequential model is inferior in the convenience of using parallel: instead of getting a validation error on each field, it is more convenient to get all the errors at once and correct them at the same time.
If you can apply an operation to a pair and get an object of the same type as a result, then you can apply such operations to lists. This is a property of monoids. For a deeper understanding of the topic, you can read the article "
monoid without tears ."
Domain Events

In some cases, it is necessary to transfer additional information. These are not errors, just something of additional interest in the context of the operation. We can add these messages to the return value of the "successful path."
Beyond the scope of this article.
- Handling errors across service boundaries
- Asynchronous model
- Compensatory transactions
- Logging
Summary. Functional style error handling

- Create a
Result
type. Classic Either
even more abstract and contains the properties Left
and Right
. My Result
type Result
only more specialized. - Use bind to convert functions to a “two-track model”
- Use the composition to link the individual functions to each other.
- We consider error codes as first-class objects.
Links
- Source code with an example is available on github
- Article on Habré based on the report with the implementation in C #