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
Joel Spolsky at Joel on Software
- They are invisible in the source code.
- They create too many possible exit points for a function.
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;
}
}
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;
}
}
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}`);
}
}
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>
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
}
}
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);
}
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.
Top comments (7)
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:
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:
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.
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.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:
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
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.
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.
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...