The art of failing fast with Option and Either

We ditched “null” and throwables once and for all. Here’s what we do instead.

Ian Davidson
Peloton-Engineering
8 min readJan 20, 2021

--

Co-authored by Ben Holmes

If you’ve built anything at scale, you’ve definitely started a function with this at least once. Maybe you’re checking an optional function parameter. Maybe you’re calling a find that might not find anything. Maybe you have “falsy” values like an empty string you want to match against. The list goes on and on…

And then there’s this pattern:

Exceptions are always bound to happen, but you never really know where they’re gonna crop up. Maybe you wrap this around some database queries that might bomb out. Heck, maybe you’re catching “null pointer exceptions” when you forget to check!

Bottom line, handling when functions don’t go your way can feel like an overly-cautious ritual. Just throw 50 null checks here, wrap some try / catch-es around those, duct tape some unit tests onto everything… and hope nothing bombs out in production 🤷‍♀️

This used to be our own, Python-heavy codebase at Peloton. But as we’ve branched into strictly typed languages and functional programming techniques, we’ve discovered some great patterns to ditch these unexpected explosions for good. In this post, we’ll explore:

  1. The inherent problem with null
  2. The road from nullable types to “Option” / “Maybe” wrappers
  3. Adapting Option-al values to error messages
  4. Mixing and matching approaches to write the sound-est logic 🚀

Alright, let’s ride 🚴‍♀️

Why null is scary

Before talking about exceptions and throwables, let’s discuss another elephant in the room: dealing with the great and powerful null. Programmers may consider it the necessary evil to represent anything that’s… nothing. It’s a convenient fallback for stubbing out new values, ignoring “absent” function parameters, quickly emptying an object, etc etc etc.

But there’s a reason behind this quote:

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

- Tony Hoare

☝️ This is coming from the inventor of the null pointer. Pulling this apart, there are some major implications to consider:

  1. Null references are easy, but that doesn’t mean they are safe.
  2. Uncaught pointers are easily overlooked (in dynamically typed languages, anyways). This causes costly errors when your unit tests aren’t quite thorough enough.

Yes, you can write perfectly safe programs with the right null checks in place, but it’s hard to guarantee you’ll find all the faults! For more on this subject, “JavaScript Joel” has some amazing examples here coming from JS’s null vs. undefined problem.

Okay, but my language has non-nullable types!

Great! This goes a long way in solving the unexpected null problem. If you use a language like Kotlin or TypeScript, you can use type checking out-of-the-box to guarantee something is never null.

Using Peloton’s ecommerce store as an example, say a user wants to add a Bike package to their cart. When they hit “submit,” we’ll send off an order that looks something like this:

In this case, we don’t want any of the accessory names or selections to be null; that means the user didn’t choose an accessory in the package! With a simple type, we can guarantee we can never make this assignment:

Great! With this sort of type system, we don’t need to worry about unexpected errors. We’re good to go as long as we convert any expected nulls to some other default value.

In those places we do expect errors, we can define our type as “nullable” like so:

So now we get the best of both worlds, only using null when we’re ready to handle it 💪

And some languages take it a step further

Type checking for nulls is nice. But some language designers (functional programming languages in particular) have dared to ask: what if we get rid of null altogether? This would mean tackling the same problem of eliminating uncaught nulls with type checking… but using a different set of vocabulary.

We’ll use the ArrowKT Kotlin library as an example. We use this across all our new backend microservices at Peloton, so it feels like a natural source of inspiration!

In short, Arrow has the concept of an Option type you can use with your standard Kotlin variables. This “wraps” around existing fields to denote that a variable might not have a value. In other words, it’s Option-al. Let’s see a before / after coming from nullable types:

That’s a new approach! Instead of our “if gating” ritual, we can use .map to retrieve the value.

  • If it’s present, we can use it inside our .map function. In this case, that value will be of type “string.”
  • If it’s absent, we’ll just short circuit without running the block.

Creating those “optional” values isn’t too bad either:

In this case, Some and None are subtypes of an Option; the former representing the presence of a value, and the latter representing “nothing.” This idea of wrapping and unwrapping prevents you from making unsafe comparisons like this:

Notice we haven’t “unwrapped” the userEmail using .map, so it won’t let us compare against a regular, unwrapped string. This avoids the issue of “falsy” values you might find in other languages, where, say, an empty string is “equal” to null.

Still, if you haven’t seen this concept before, you might be thinking what? Why are we inventing a “None” type? That’s what null is for! This isn’t a bad take to be honest. The idea of nullable vs. non-nullable types covers most of the same bases that Optional types do. In fact, the ArrowKT library is deprecating the Option type entirely because Kotlin’s built-in nullables are just as good!

This doesn’t mean our “wrapping / unwrapping” paradigm is gone for good though. Let’s take this idea a step further with exception handling

What if a “nothing”… told you WHY it was nothing?

This is a natural extension of our Option type. As you noticed, any Options with a value of None don’t really contain anything. In practice, this has the same bad implications for readability that a null stand-in might.

Wait, why is it None? Were there really no matching packages? Did our database query timeout? Was the user’s query params badly formed?

What’s worse, this “retrieveBikePackageFromDB” might throw an exception we’re unaware of! But in order to know for sure, we’d have to either:

  1. Be familiar enough with the codebase to know what exceptions might pop out
  2. Preemptively wrap it in a try / catch just to be safe (even if it’s unnecessary).

Now, we need to handle both None results and exceptions, whatever those exceptions even look like 😬

But let’s consider: If None() is a constructor that could wrap around something… why can’t we slap an error message in there? Or an error enum we can match up against? That would immediately let us know what sort of exceptions we could have, and let us differentiate between errors and “empty” responses!

Enter: the Either type ✨

Telling your Left from your Right

Either is a bit like the Option type. But instead of None and Some, we have Left and Right. By convention, Left is used to represent the error path, and Right is used to represent the good path.

Let’s look at a typical example of exception throwing code for validating a user’s email and see how we can improve it with Either.

Notice anything odd about the code? The type signature doesn’t really say what the function can do. Imagine an even more deeply nested bit of code, and having to read through it to determine what errors you need to catch. We’ve all been there. Just like Maybe, Either can be used to improve the safety and readability of your code.

Now we can see from just the type signature what this code really does! Anyone calling this function doesn’t have to go on an Exception hunt, or add a catch-all-exceptions escape hatch.

Just like our trusty Option type from before, we can use .map to access the inner value.

Now we’re validating the email before sending it. If validateEmail were to return a Left, the computation would stop and not send the email, while returning the Error. If it’s a Right, we’ll send the email as usual.

Can we combine the powers of Option and Either? Well, of course!

First we convert the userEmailForNewsletters to an Either. If it’s None, it turns into Left(“Email is missing”). If it’s Some, it turns into Right(unwrappedUserEmail). Then we flatMap instead of map to prevent overly nesting, since validateEmail returns an Either. Finally we map over that either and send the email or return the error.

Code like this can be extended simply with more maps, and at each step you are safe in the knowledge that all prior steps have succeeded — Since the wrong path stops computation and returns early.

Wrapping up

We’re still new to this shift away from nullables and throwables. But from our experience so far, it’s allowed us to be more thoughtful about handling our exceptions. With throwables, it was easy to just “catch everything” and say “Oops, something went wrong!” Now we have the power to write granular, descriptive error responses for all of our clients.

This has a promising future as we move towards a microservice architecture, where each team needs to understand the inputs and outputs of each service to cross-communicate. If we can be more descriptive with our error handling, we can greatly reduce confusion between teams and focus more on our user experience 😁

--

--