Exceptions vs error values

Exceptions vs error values

Exceptions vs error values has been a debate in error handling for decades. In this article we'll examine the pros and cons of each.

Exceptions vs error values has been a debate in error handling for ages. Some people have firm stances on them. For example, in the book Clean Code, Uncle Bob recommends exceptions. In his post on Exceptions, Joel mentions that he prefers error values.

Programming languages have also taken stances. Popular languages such as C# and Java traditionally use exceptions. Languages like Rust use error values.

In this article we'll examine some of their similarities and differences. We'll also provide suggestions about when to use which.

Basic examples of exceptions and error values

Just for a quick introduction, here are some examples of exceptions and error values.

If you're already familiar with them, then please skip to the next section.

Here's an example of throwing and catching an exception in C#:

public class Example
{
    public void Foo()
    {
        try
        {
            Bar();
        }
        catch (IndexOutOfRangeException ex)
        {
            // handle error
        }
    }

    public void Bar()
    {
        if (true /* some condition to check if something went wrong */)
        {
            throw new IndexOutOfRangeException("Some error message");
        }
        else
        {
            // normal program execution
        }
    }
}

In the code above, Bar throws an exception. The exception is caught and handled in Foo, in the catch block.

Here's the same thing in JavaScript:

function foo() {
  try {
    bar();
  } catch (error) {
    // handle error
  }
}

function bar() {
  if (true /* some condition */) {
      throw new Error("Error message");
  } else {
      // normal program execution
  }
}

Error values can be implemented in different ways. One way is for a function to return either an error or a normal value.

For example:

function foo() {
  const result = bar();
  if (result instanceof Error) {
    // handle error
  } else {
    // normal program execution
  }
}

function bar() {
  if (true /* some condition */) {
    return new Error('Error message');
  } else {
    return 42;
  }
}

In the code above, bar can return either an error or a normal value. foo checks the return value. If it was an error, it handles it. Otherwise, it continues normal program execution.

You can also use error values by returning a single object. The object should have fields for both the error and the normal return value. For example, you could use a tuple, or an object with properties. If there was an error, the value should be empty. For example {error: new Error('Message'), value: null}. If there wasn't an error, the error value should be empty. For example {error: null, value: 42}.

Here's a code example:

function foo() {
  const result = bar();
  if (result.error !== null) {
    // handle error
  } else {
    // normal program execution
  }
}

function bar() {
  if (true /* some condition */) {
    return {error: new Error('Error message.'), value: null};
  } else {
    return {error: null, value: 42};
  }
}

In the code above, bar always returns an object. If something goes wrong, the object will have a value in the error field. Otherwise, the error field will be null.

Similarities between exceptions and error values

Exceptions and error values are fairly similar. In fact, some newer programming languages such as Rust and Swift eliminate most of the differences between them.

The most important thing about both of them is that they act as different return values from a function / method. The different return values should lead to different code execution paths.

They also share a big downside. It's easy to mess up with both of them.

With an exception, you may:

  • forget to catch it
  • wrongly assume that some code higher in the call stack will catch it
  • accidentally catch it higher in the call stack in a place that's not prepared to handle it properly

Also, you can completely avoid checking error values.

It's very easy to forget or mess up. Even if you don't, someone else might. So, you have to be very diligent.

Or, you can use a programming language that forces you to check all errors. (More on that later.)

Differences between exceptions and error values

Exceptions and error values have some differences:

Performance

Throwing and catching exceptions are commonly considered slow. Returning error values is fast.

However, exceptions are supposed to be "exceptional" (thrown very rarely). In practice, this means that the performance of your application shouldn't be negatively affected by using them.

Crashing the program vs silent bugs

Uncaught exceptions crash the program. More rarely, exceptions can also result in silent bugs (if you catch them higher in the call stack without intending to).

Unchecked error values result in silent bugs.

Exceptions are better in this case. As explained in how to respond to errors, crashing the program is a better default option than silent bugs.

Bubbling

Exceptions can "bubble" up the call stack. An exception that's not caught in a catch block will be thrown in the caller (the previous code in the call stack). If it's not caught there, the process will repeat. If it reaches the end of the call stack, the program will crash.

Bubbling is both good and bad.

The benefit is that it's very convenient. You can have a single try / catch block in some parent function. The exception will propagate to it and will be caught there.

The downside is that the flow of execution is not explicit. You have to keep track of it yourself. You also have to remember which exceptions are caught where in the call stack.

This can put you into a bad situation. Sometimes you might not remember or know if an exception will be caught or not, or where it will be caught, or by what.

In comparison, error values are standard return values. If you want them to propagate, you have to propagate them manually. You have to manually return them across different functions / methods, all the way up the stack.

The benefit of this is that it's very explicit. It's very easy to track and reason about. The downside is that it's very verbose. You need many return statements across many different function / method calls.

Note that you can technically manually propagate exceptions if you want to. However, that's not common practice. For more details on this please see "checked exceptions" in a later section.

Suitability in functional programming

Generally, exceptions are less common in functional programming.

That's because functional programming promotes immutability and pure functions.

With exceptions, sometimes you need to break immutability. For example, often, you need to declare variables outside of try / catch blocks and then mutate them in try / catch.

Here's a code example:

let a;
try {
  a = new Something();
  // do stuff with `a`
} catch (error) {
  // handle error
} finally {
  a.close();
}

Also, thrown exceptions are not standard return values. This messes up the "pure function" point.

Exceptions and error values in some newer languages

Some newer languages, like Rust and Swift, change things up a bit.

Most importantly, they force you to check all error values and thrown exceptions. This means that you can never forget to check for errors or to handle exceptions.

In the case of Swift, it also makes exception bubbling more explicit. It still allows exceptions to propagate automatically. However, it requires intermediate functions (that an exception will propagate through), to be marked with the keyword "throws".

This additional explicitness makes exceptions easier to track throughout your code.

The downside is that it makes things more verbose.

(Rust uses error values, which you have to propagate explicitly anyway.)

Which should you use?

Overall, it seems like this is a question of robustness and amount of safety measures vs verbosity.

Enforcing error checking and having explicit error propagation have obvious benefits. It makes it much harder to forget to do your error handling. You'll have to intentionally ignore it to avoid it.

However, verbosity has downsides too. It can can make code less readable. It can also make it harder to make large changes to code. This can be especially prominent if you're propagating everything manually.

For example, imagine that you change a low-level function (or add a new one) to sometimes return an error value. That error may need to be handled at a higher-level function. This means that you'll need to add code to every intermediary function to keep propagating the error.

That's a large change. In comparison, if you added an exception that bubbled automatically, you would just add a try / catch block at the high-level function and you'd be done.

So it's up to you to decide where you stand on the safety measures vs verbosity scale.

For maximum safety measures, you should probably use a language that forces you to check all errors and forces explicit propagation of them. The downside is that the error handling will be more verbose.

One level lower in safety is to use error values. I regard these as more robust than throwing exceptions. That's because propagating error values is more explicit than bubbling exceptions. The downside is that there's more verbosity. Also, note that you need to be very diligent with these. If you forget to check an error, you'll get silent bugs. Unchecked error values are worse than uncaught exceptions.

Otherwise, go for throwing "normal" exceptions (such as the ones in Java, C# and JavaScript). They're the least verbose. This doesn't mean that you can't create robust programs with them. It just means that it's up to you to be diligent with errors and to track everything.

It's probably also a good idea to consider the convention in your programming language. Some programming languages prefer exceptions. Some others prefer error values.

My personal preference is to lean towards higher safety for larger scoped and more critical projects. For smaller scoped projects, I lean towards less verbosity and more convenience (exceptions).

Final notes

So that's it for this article. I hope that you found it useful.

As always, if any points were missed, or if you disagree with anything, or have any comments or feedback then please leave a comment below.

For the next steps, I recommend looking at the other articles in the error handling series.

Alright, thanks and see you next time.

Credits

Images:

  • Duelling Legos - Photo by Stillness InMotion on Unsplash
  • Typewriter and laptop - Photo by Glenn Carstens-Peters on Unsplash
  • Post-it notes - Photo by Will H McMahan on Unsplash