Navan Tech Blog
Functional Programming: A Better Way to Handle Errors?

Functional Programming: A Better Way to Handle Errors?

Antonio Pellizzaro

2 Mar 2023
5 minute read
Functional Programming: A Better Way to Handle Errors?
Functional programming has a steep learning curve — but when it comes to handling errors, the investment pays off.

Error handling is typically an afterthought for programmers. In their haste to get a product to the finish line, they focus entirely on the task at hand, only to find themselves facing a tangle of exceptions and error codes down the line.

I wanted to try something different: acknowledge from the outset that errors will occur and construct code equipped to handle any error that arises. When I began designing Navan’s infrastructure to manage fees, I decided to test out functional programming — an approach that not only consolidates error handling but also allows for clean, consistent code.

How? It comes down to a nifty concept called the “result monad.”

The result monad, explained

A monad is essentially a container. You can think of it like a wrapped gift. The container may or may not contain a value — we don’t know, since we can’t see past the wrapping paper. We can, however, transform its contents using one of the following two operations:

  • Create a monad from a value
  • Combine two monads

Java developers are likely already familiar with a handful of monads, like the reactive Mono<> and Flux<> monads, or the type Optional<> monad.

If the result monad contains a value, we deem the operation a Success<>. If it contains a null value, then it returns an Error<>.

We can manipulate and interact with monads using functions that combine two monads, such as flatMap() or map(). To create a Result, we can either use Result.of() to create a Success object or Result.ofError(Exception e) to create an Error object.

At this point we have all we need to make our functions as pure as possible, including error handling. We need to make sure that every function always returns a Result<> so we manipulate the values through the transform methods.

Let’s consider an example. Say we have a function that divides two integers. Because there might be error conditions, we return a Result<>.

In the real world, our program interacts with libraries, drivers, databases, and other elements, each of which may be a source of errors. To get a handle on them, we first need to wrap each of these calls into access functions that return a Result<>. This step ensures that we will always receive a Result<>, without having to worry about exceptions thrown or checking for null values.

Functional Programming - Result Monad - Code Snippet 1

When the Result<> is a Success<> (i.e., it contains a value), we can work with it. Otherwise, the operation will be ignored.

Another example: Let’s assume that we need to read a value with a specific ID from a repository. The repository could either return a value or throw an exception. But by implementing a simple function, we can catch any exceptions and return the appropriate Result<>.

Functional Programming - Result Monad - Code Snippet 2

We can also combine different results. For instance, we can implement a function that takes a user ID and updates that user’s bank account to reflect a specific charge.

To do this we need a UserService, which retrieves a user record (wrapped in a Result<>), and a BankService, which retrieves a user Account (also wrapped in a Result<>). The function returns the new account balance.

Now, suppose we have the following function definition:

Functional Programming - Result Monad - Code Snippet 3

Then the code for updating a user account will look like this:

Functional Programming - Result Monad - Code Snippet 4

Notice that there is no error handling. That’s because the flatMap implementation will only apply the mapping function on a Success<>; it will not do anything on an Error<>. The final returned result will be either

  • a Success<> containing the final value or
  • an Error<> with an exception indicating the source of the error

If the initial findUser() step returns an Error<>, the next steps — locating the user’s account and updating their balance — will not be executed. Instead, an Error<> will be returned, with a note indicating its source (database exception, http exception, user not found, etc.).

The key idea here is that we ignore the result monad’s contents while working with it. Instead, we continue to transform it until we arrive at the final result. At that point, we can access its contents and take action if there is an error.

For example, when responding to a rest call, we would wait until right before returning the http response before opening the Result<> and deciding what httpStatus to return.

The implementation of flatMap() in Success<> and Error<> is fairly simple:

Success<>:

Functional Programming - Result Monad - Code Snippet 5

Error<>:

Functional Programming - Result Monad - Code Snippet 6

As we can see, a very simple implementation can bring us a nice error-handling paradigm.

Error manipulation

In most cases, of course, we want to do more than just return an error — we also want to do something about it, by logging it or finding a way to recover it.

For this, we use flatMapError, a dedicated version of flatMap, which includes parameters to indicate the class of the exception we want to manage. Here’s the implementation:

Success<>:

Functional Programming - Result Monad - Code Snippet 7

Error<>:

Functional Programming - Result Monad - Code Snippet 8

Lastly, we need to access the final Result value (or Error) to determine the next step. For this, we can use the get() method to retrieve the result value; it will throw an exception in case of an Error<> type.

Conclusion

The result monad is just one example of how to handle errors efficiently. It isn’t perfect; it forces programmers to work within a very rigid structure, which for some people, can feel limiting or forced.

That said, functional programming makes possible an entirely new error-handling paradigm. Using the result monad to wrap values or exceptions has helped us to write exceptionally clean, consistent code with no thrown exceptions.

It’s not the only tool, of course, and it’s not the only pattern — but it might make sense to try it out in your job.

Return to blog

More content you might like