A lot of applications seem to either only use runtime exceptions or only use error monads like the Optional for error handling. In this blog I will try to dive a bit deeper into when to use one over the other (tldr: you probably need both)

Before we start, lets get clear what the difference is between using a runtime exception and an error monad.


Let’s start with the runtime exception.

* Will throw an ArithmeticException if b equals 0
public double divide(double a, double b) {
  if (b == 0) {
    throw new ArithmeticException();
  return a / b;

The advantage here is that the function signature (double, double → double) is very clean. This means that as long as it doesn’t go wrong, no extra effort is required for error handling. The problem however is that a RuntimeException means that the current thread should be crashed unless the exception is caught. There is however no way of knowing this behavior except reading the javadoc or the function body. This can lead to unexpected behavior as it forces all callers of this function to remember to do error handling if needed.

So let’s see what monads can bring us. Java has the Optional monad for error handling and it looks something like this.

public Optional<Double> divide(double a, double b) {
  if (b == 0) {
    return Optional.empty();
  return Optional.of(a / b);

Now the caller is forced to make a decision to handle the fact the function can go wrong. This decision is compiler checked, since you need to call one of these methods to get the value out of the optional.

var myOptional = Optional.of("myString");
var x = myOptional.orElse("default");
var y = myOptional.orElseGet(() -> "lazy default");
var z = myOptional.orElseThrow();

As we can see here we either need to choose a default value, or choose to turn it into a RuntimeException. We can of-course also use map and flatMap operations on it if we want want to delay making a decision and still perform operations on the happy path. This works because the function we supply to map is only executed if the Optional has a value.

var myOptional = Optional.of("myString");
Optional<Integer> optionalWithLenght = myOptional.map(s -> s.length());

So at the cost of now having an Optional as a return type (which can clutter up your code) we now get the benefit of signaling to the caller this function can fail. We also give different callers an option on how to respond to the failure. One caller might turn it into a RuntimeException, another caller might supply a default and yet another caller might delay the decision, call .map and return an Optional itself.

So when would you use a RuntimeException vs and Optional? A good way to look at them is in terms of recoverable vs unrecoverable errors.

Recoverable errors

These are errors that can be recovered from. Being recovered from can be defined quite loosely, some examples of "recovering" are

  • Supplying a default value

  • Calling a cache if a database is not available

  • Validation (sending back a different status code with a different body is a form of recovering)

In this case the Optional tends to be quite useful, since it naturally gives (and even forces) the function caller a way to recover. Using a RuntimeException here is possible, but it requires discipline to remember to catch all possible errors and deal with all of them in the correct way.

Unrecoverable errors

These are errors that when they happen it is ok to just let the application crash. Some examples are

  • If your application is a webservice these tend to be things that would return a 500 internal server error.

  • If for example you are building a commandline tool that needs to read a file and the file doesn’t exist, there is probably also no way to recover so crashing is fine.

Here a RuntimeException shines. If the problem cannot be recovered from in any context then using and Optional only makes the code more complex than needed. In these cases it is fine and even encouraged to use a RuntimeException based flow.


It is good to look at errors in terms of recoverable in any context, or not recoverable in any context. Viewing errors in this way can help you make a decision on when to use an Optional vs when to use a RuntimeException.