Exceptions vs error values

Exceptions vs error values has been a debate in error handling for decades. 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.

Essentially, they can be thought of as different ways to return something from a function / method. That’s the most important thing about them.

Exceptions and error values also share a big downside. You can forget to catch an exception. Or, you can wrongly assume that some code higher in the call stack will catch it. 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 exceptions is 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 won’t be negatively affected by using them.

Crashing the program vs silent bugs

Uncaught exceptions crash the program. 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.

Bubbling

Exceptions can "bubble" up the 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 of whether 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 used in functional programming.

That’s because functional programming promotes immutability, pure functions and certain abstractions like the Either monad.

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.

Finally, exceptions don’t work with things like the Either monad.

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".

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

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

The downside is that it makes things more verbose.

Which should you use?

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

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 exceptions. 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.

About verbosity: Verbosity can make code less readable. It can also make it harder to make large changes. 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.

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).

Some sidenotes about error codes and exceptions

Just for completion, here are some notes on some things you may come across.

A note on error / status codes

Error / status codes are something you can return instead of an error value.

They are IDs, usually strings or numbers. The IDs denote particular states, such as success or a type of error. To see what the codes actually mean, you have to check some documentation where the IDs and errors are listed.

They’re not really used except in rare cases. For example:

  • if your programming language doesn’t support error types
  • if you need to minimise memory usage in your program
  • if you need to communicate statuses to a different program, where you can only pass messages which are strings and not objects

A note on checked exceptions

Checked exceptions are exceptions that don’t bubble automatically. If you want them to propagate you need to do it manually. You have to explicitly catch them at every level and re-throw them.

Some people love checked exceptions because of the explicitness they provide. Others hate them because of the verbosity.

Even though they propagate like error values, error values seem to be more accepted.

One reason for this is because error values are standard values. They can be handled in all of the ways that values can be handled. Checked exceptions are more restrictive. They can only be handled in try / catch blocks.

Another reason is because checked exceptions can’t be manipulated as easily as generics in most languages. This means that you can use generics for the return error value of a function, but you may not be able to use generics for the exception.

If you want to avoid all exceptions

If you’re using error values, you probably want to avoid exceptions. However, you may be using other code which throws exceptions.

In this case, do what Joel does to avoid exceptions:

  • don’t throw your own exceptions
  • catch and handle every possible exception immediately

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:

  • Bowing Legos – Photo by Stillness InMotion on Unsplash
  • 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
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments