Either - Error Handling in Functional Programming

Sandro Maglione

Sandro Maglione

Functional programming

Either is used in functional programming to handle errors. Either is an alternative to try / catch / throw. These are 2 different strategies to deal with errors in your app.

In this post we are going to learn:

  • What is Either
  • How Either is defined
  • Example of Either and try / catch
  • Comparison between Either and try / catch
  • Advantages of using Either

After reading this post you will have learned a new powerful strategy to work with errors in your app 🎯

try / catch / throw - Imperative error handling

In imperative languages the usual strategy for handling error is using try / catch blocks.

The idea is simple: whenever you encounter some unexpected situation (missing data, wrong values, inconsistencies) you throw an Exception using the throw keyword.

double divide(int a, int b) {
  if (b == 0) {
    throw FormatException("Cannot divide by 0");
  }
 
  return a / b;
}

throw tells the app that some "error" happened. Now the app is at risk!

The default behavior at this point is for the app to crash: if no other part of the code "catches" this exception, the app will simply stop working, showing a blank screen or closing itself.

This is where "Error Handling" comes in. You use a try / catch block to execute any code that may fail.

try {
  final result = divide(val1, val2);
} catch (e) {
  /// Do some "Error Handling" here πŸ‘ˆ
}

This code is responsible to stop the exception from propagating and manually define a strategy for dealing with an error (showing message to the user, logging the issue).

Either - Functional programming error handling

In functional programming the approach is completely different: it's called Either.

Either is a type that can contain only 2 possible values, either one or the other (no both!):

  • An error value (called Left by convention)
  • A successful return value (called Right by convention)
Either<String, double> divide(int a, int b) {
  if (b == 0) {
    return Left("Cannot divide by 0"); /// `Left` (Error ⛔️)
  }
 
  return Right(a / b); /// `Right` (Success β˜‘οΈ)
}

Left is responsible to handle every error: in case of exceptions the function will return a Left containing some value which represents the error.

Right instead is the "normal" return value in case no error happens.

Either defines a return value for both the success (Right) and error (Left) case, making explicit what can go right and what can go wrong

How can a function have different return values (Left in some case, Right in another)?

Generic types - Either in Object Oriented languages

In Object Oriented languages the Either type is implemented by using inheritance.

The Either type is defined as an abstract class. This means that Either cannot be instantiated by itself.

Either also defines 2 generic type parameters:

  • L: Type of the value contained inside Left (error type)
  • R: Type of the value contained inside Right (success response type)
///                  πŸ‘‡ `L` and `R` generic types
abstract class Either<L, R> {}

We then define 2 more classes that implement Either:

  • Left: Concrete class that represent an error. As such, Left contains a value of type L
  • Right: Concrete class that represent a successful response. As such, Right contains a value of type R
abstract class Either<L, R> {}
 
class Right<L, R> implements Either<L, R> {
  final R value; /// Success value β˜‘οΈ
  const Right(this.value);
}
 
class Left<L, R> implements Either<L, R> {
  final L value; /// Error value ⛔️
  const Left(this.value);
}

Example - try / catch or Either

Let's look at a more detailed example to understand the difference between these 2 approaches.

Let's imagine we have the following requirements to implement:

Given a username, we need to check if it is valid and return it in such case. A username is valid if 1️⃣ it is not already used, if 2️⃣ it is longer than 4 characters but shorter than 16, and if 3️⃣ it does not contain symbols.

Let's also imagine that we are given the isNotAlreadyUsed function and hasSymbol function by another member of the team:

/// Someone else took care of implementing these two πŸ‘‡
bool isNotAlreadyUsed(String username) => ...
bool hasSymbol(String username) => ...
 
??? checkUsername(String username) => ... /// We are working here πŸ‘ˆ

Imperative error handling

Below you can see this requirements implemented using throw + try / catch.

String checkUsername(String username) {
  if (isNotAlreadyUsed(username)) {
    throw FormatException("Username is already used");
  }
 
  if (username.length < 4 || username.length > 16) {
    throw FormatException("Username must be between 4 and 16 characters");
  }
 
  if (hasSymbol(username)) {
    throw FormatException("Username cannot contain a symbol");
  }
 
  return username;
}

Quite simple: you check every erroneous formatting and you throw an exception in such cases.

Wait! That's not all. This code is only responsible to throw errors, it does not handle them. We also need to show the main function to see error handling in action:

void main() {
  try {
    final validUsername = checkUsername(username);
  } catch (e) {
    print("There username is not valid");
  }
}

Note: For this example, we simply print a message if something goes wrong. In a real app, you may want to define some custom exceptions and change your error handling strategy based on the type of exception.

Functional programming error handling

And now let's look at the same requirements implemented using functional programming and the Either type.

Either<String, String> checkUsername(String username) {
  if (isNotAlreadyUsed(username)) {
    return Left("Username is already used");
  }
 
  if (username.length < 4 || username.length > 16) {
    return Left("Username must be between 4 and 16 characters");
  }
 
  if (hasSymbol(username)) {
    return Left("Username cannot contain a symbol");
  }
 
  return Right(username);
}

Actually, the code looks nearly exactly the same! 2 main changes:

  1. Change the return type to Either<String, String>. This makes explicit that this function may go wrong.
  2. Change throw with Left, retuning a value also in case of errors.

We then need to handle the value of Either when calling the function. We use the match method:

void main() {
  final usernameEither = checkUsername(username);
  usernameEither.match(
    (error) => print('$error'), /// Error ⛔️
    (username) => print('Valid username: $username'), /// Success β˜‘οΈ
  );
}

Note: Another way to implement the function using the full API of the Either type would be the following:

Either<String, String> checkUsername(String username) =>
    Either<String, String>.fromPredicate(
      username,
      isNotAlreadyUsed,
      (_) => "Username is already used",
    )
        .flatMap(
          (username) => Either<String, String>.fromPredicate(
            username,
            (username) => username.length < 4 || username.length > 16,
            (_) => "Username must be between 4 and 16 characters",
          ),
        )
        .flatMap(
          (username) => Either<String, String>.fromPredicate(
            username,
            hasSymbol,
            (_) => "Username cannot contain a symbol",
          ),
        );

This function uses the fromPredicate and flatMap methods.


Compare try/catch and Either for error handling

We can notice some advantages and disadvantages with these 2 approaches:

  • try / catch is easy and familiar: try / catch looks just like an if / else statement. Therefore it is easy to explain and quite straightforward: do you expect an error? Just wrap everything at some point with try / catch and you are sure that nothing bad will happen.
  • try / catch requires less code: Since you do not need to manually handle all possible error cases, try / catch tends to require less code to achieve the same result compared to Either
  • Either is more safe: Using throw we risk to crash the app if we forget to handle the exception. Since there is no compile time check, this situation may lead to errors at runtime for our users. Either instead never crashes the app. Instead, Either requires you to handle the error at compile time
  • Either has a more powerful API: By using Either you have access to a more extensive API to recover from errors, update the return value, chaining functions and more
  • Either makes the error explicit: Using Either, the return type of the function itself declares that something may go wrong. Using throw instead you are required to read the definition of the function to know that it may fail

Either and Errors

Either is designed to handle all possible errors and exceptions.

There are cases in which we actually want some unrecoverable error to crash the app.

In fact, Either deals with recoverable errors: errors that are somehow expected and that we want to handle.

When we encounter unrecoverable errors or particularly exceptional cases that must never happen, it is safer to throw and crash the app.

A good analogy is the following1:

"Nurse, if there's a patient in room 5, can you ask him to wait?"

Recoverable error: "Doctor, there is no patient in room 5." Unrecoverable error: "Doctor, there is no room 5!"


Now you know how the Either type works and how it compares to try / catch. This was an overview of what the Either type has to offer. The Either API is actually more powerful and it can help in many more cases.

We are going to learn more about it in future posts. You can subscribe to my newsletter here below to stay up to date with new posts and updates πŸ‘‡

Thanks for reading.


Footnotes

  1. https://softwareengineering.stackexchange.com/a/150851 ↩

πŸ‘‹γƒ»Interested in learning more, every week?

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 400+ readers.