loading...

Exceptions Considered Harmful

pianomanfrazier profile image Ryan Frazier Originally published at pianomanfrazier.com on ・5 min read

The reasoning is that I consider exceptions to be no better than “goto’s”, considered harmful since the 1960s, in that they create an abrupt jump from one point of code to another. In fact they are significantly worse than goto’s

  1. They are invisible in the source code.
  2. They create too many possible exit points for a function.
Joel Spolsky at Joel on Software

How do we deal with uncertainty in our code?

If something goes wrong in our code we need to know about it, preferably without crashing our program. When I come back to the code months later or I am using someone elses code I want the compiler to help me handle errors gracefully.

Here are several patterns that I have seen, my own code included.

Pattern 1 - return true or false

function doWork() : boolean {
    // do some SIDE EFFECT
    let result = doWork();
    this.some_member_variable = result;

    let success = result !== null;
    if (success) {
        return true;
    } else {
        return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

Side effect's make it harder to reason about what your code does. Pure functions, side effect free functions, are also easier to test. Also if there was a failure you can't send a message to the function caller.

Pattern 2 - return null if failed

In the next examples, let's assume that our database stuff are synchronous to make things a bit simpler.

Instead of returning true or false we could return the value or a null value.

import DB from 'my-synchronous-database';

function getUser(id : UserID) : User | null {
    const user = DB.getUserById(id);
    if (user) {
        return user;
    } else {
        return null;
    }
}
Enter fullscreen mode Exit fullscreen mode

This is slightly better, now that we don't have a side effect. However we still have no error message and we better make sure to handle that returned null value or our program will explode.

This eliminates the side effect but now creates a new problem.

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 at QCon London 2009, see wikipedia

Pattern 3 - throw exception

Our other choice is to throw an exception.

import DB from 'my-synchronous-database';

function getUser(id : UserID) : User {
    const user = DB.getUserById(id);
    if (user) {
        return user;
    } else {
        throw new Error(`Cannot find the user by id ${id}`);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we have an error message but now we introduced another side effect: the exception. If you don't catch the exception, in most cases, your program will crash.

In JavaScript there is no way I can tell by using a function if it will throw or not. Java helps because the tooling will warn you that you are using a throwable function. Still no one likes seeing a nullExceptionPointer in Java land. Not fun.

Pattern 4 - return a result type

What if we wanted to both return an error message if something goes wrong and also not introduce side effects.

This is the Result type.

This thing is baked into the standard library of newer programming languages like Rust and Elm. We have std::result in Rust and the Result Type in Elm. Some newer languages don't implement exceptions and treat errors as data like Go, Rust, and Elm.

Since this article is using TypeScript, I'm going to use the library neverthrow but there are others to choose from. This will also work in plain JavaScript too.

Let's look at neverthrow's Result type.

From the neverthrow docs:

type Result<T, E> = Ok<T, E> | Err<T, E>
Enter fullscreen mode Exit fullscreen mode

Ok<T, E>: contains the success value of type T

Err<T, E>: contains the failure value of type E

And here it is in action.

import { Result, ok, err } from 'neverthrow';
import DB from 'my-synchronous-database';

type DBError = string; // type alias for error message

function getUser(id : UserID) : Result<User, DBError> {
    const user = DB.getUserById(id);
    if (user) {
        return ok(user); // return instance of OK
     } else {
        return err(`Cannot find the user by id ${id}`); // return instance of Err
     }
}
Enter fullscreen mode Exit fullscreen mode

This is an improvement because there are now no side effects and we can return an error message if something goes wrong. I know that when I use this function I will always get a Result.

const userID = 1;
const userResult : Result<User, DBError> = getUser(userID);

if (userResult.isOK()) {
    console.log(userResult.value);
} else {
    console.log(userResult.error);
}
Enter fullscreen mode Exit fullscreen mode

If you try to retrieve userResult.value before you have checked isOK() the TS compiler won't let you. Pretty awesome.

JavaScript tooling

tslint-immutable is a plugin for TSlint that has several options to prevent throwing exceptions. See this set of functional programming rules for TSlint here. Enable no-throw and no-try.

And here is a similar set of rules for eslint.

Other libraries and languages

These ideas are also being explored in other languages. Here are some libraries I found.

C++ std::optional, optional<T>, is a safer way than just returning null. The optional can be empty or it can hold a value of type T. It does not hold an error message. This type is also called Maybe in elm and elsewhere.

C++ Result is a header only library that implements Rust's Result<T, E> type. This type can hold the value or an error.

Python result another Rust inspired result type.

If you want to explore more typed functional programming in TypeScript, check out purify, true myth, or the full featured fp-ts.

Discussion

pic
Editor guide
Collapse
elmuerte profile image
Michiel Hendriks

It's right there in the name: exception. Raising one should be an exceptional case. But this is quite commonly not followed.

Pattern 2 is a good pattern. Get a user which does not exist, then you get nothing. There is no error condition here. Returning an optional is just as fine, I don't really see the point of using optionals in most cases and it does not remove the null check. Optional.isPresent() is a null check. The only difference is that Optional is more explicit that a value might be missing. I much more prefer annotating a method that a null check is required, because static code analysis can verify this. Where getUser().get().getName() will produce a null deferences and is more difficult to check in static analysis.

But, if getUser() does a database call, and for some reason the connection gets broken. This is exceptional. You would not return null in this case, or an Optional with no value. You need to propagate the exceptional case. Pattern 4 can be horrible, because that means that you need to add that construction everywhere and constantly deal with it. Much like having to deal with checked exceptions. (I prefer checked exceptions, because it make you have to deal with exception cases.)

What would be awesome if exception handling and return type (and optionals) could be part of the language:

function User getUser(id) {
    // ...
}

// Returns a user or null. Can throw exceptions.
User user = getUser(x);
// Returns an optional with a possible value. Can still throw exceptions.
Optional<User> optUser = getUser(x);
// Returns a result type, with a possible value. Or a result with an error. 
// Does not throw exceptions, they would be in the result's error.
Result<User> resUser = getUser(x);

All the same method. But the language will take care of wrapping it nicely in the format you want to deal with. Sort of, auto boxing for return values.

And if you extend this with pattern matching:

match (getUser(x)) {
    Ok(User): // got an user
    Missing: // got null
    Error(E): // got an exception
}
Collapse
pianomanfrazier profile image
Ryan Frazier Author

I suppose if static analysis can check that your function might return null it's not too evil. I would just rather have the return type be explicit and have the compiler force me to deal with the uncertainty. So I prefer pattern 4 even if you have to deal with it more. I have discovered that when refactoring out exceptions there were a lot of boundary cases I hadn't considered that I am now forced to deal with.

Collapse
elmuerte profile image
Michiel Hendriks

I have discovered that when refactoring out exceptions there were a lot of boundary cases I hadn't considered that I am now forced to deal with.

And that's why I prefer to use checked exceptions. It forces you to deal with it. There are cases where unchecked exceptions are alright, like constraint violations on inputs. (e.g. null even though I said it should not be null.)
But parsing functions, e.g. string to X, should always throw checked exceptions. But this is quite often not the case, so I need to lookup the documentation on what to expect when it's garbage.
Or various utility functions which are not forgiving, and throw exceptions even though a safe response is just as valid. For example "foo".substring(1,10) could have simply returned "oo" instead of throwing an out of bounds exception. Basically try to avoid creating cases where people have to deal with errors.

Collapse
ashleyjsheridan profile image
Ashley Sheridan

Exceptions aren't harmful, unhandled exceptions may be.

Consider the example of an API endpoint that allows a user to purchase an addon for a service. How do you handle that with boolean or non-exception object returns when any of the following occur:

  • user wasn't authenticated to use the endpoint
  • user doesn't exist
  • addon doesn't exist
  • users current service subscription doesn't allow addon
  • user has no valid payment details to allow addon purchase
  • addon isn't otherwise available

The if/else logic and null object acrobatics required just to avoid exceptions would be ridiculous.

I'm not saying we pass the exceptions to the user, but not using them in your code doesn't make sense

Collapse
pianomanfrazier profile image
Ryan Frazier Author

Languages like Go, Rust, and Haskell don't have exceptions and they figured out these problems.

I knew this article would ruffle feathers when I wrote it. I wanted to present an alternative approach. I prefer explicit handling of uncertainty rather than leaving escape hatches throughout the code.

Time has shown us that no matter how careful we are Java programs will eventually throw an uncaught nullexceptionpointer. We need better mental models not more disciplined programmers.

Collapse
ashleyjsheridan profile image
Ashley Sheridan

I was in agreement with you about handling uncertainty properly, and exceptions allow you to do exactly that.

The fact is, we will undoubtedly need to work with 3rd party libraries et al in our code. If the language supports exceptions, then we will need to handle them. It makes sense to use them if we need to handle them at some point anyway.

Plenty of incredibly popular and powerful languages use exceptions to handle errors, that's their solution to handling the error problem. If Go & Rust don't, that's fine, it doesn't make them bad languages or mean that exceptions are bad or to be avoided.

Thread Thread
pianomanfrazier profile image
Ryan Frazier Author

My usual pattern with 3rd party libraries is to catch the exception immediately around the function call then convert it to a Result. Some functional libraries even have a helper function for this. See Purify's encase function gigobyte.github.io/purify/adts/Eit...