We don't need Throw
Exceptions, a brief introduction
In many programming languages, including JavaScript, there is a mechanism for
handling errors or unexpected situations called exceptions. When we throw an
exception, the normal flow of the program is interrupted, and the control is
passed to a particular block of code called a catch
clause, where we or
whoever uses our code can handle the exception or re-throw it. If there is no
catch
clause in the current scope, the exception propagates up to the
following scope until it reaches either a catch
clause or the top global
scope. If an exception reaches the global scope without being caught, it causes
an unhandled exception error and terminates the program.
So, what’s the problem?
Exceptions can be thrown by the language itself (for example, when parsing an
invalid JSON string) or by the developer using the throw
statement. The fact
that the developer can terminate the program from pretty much anywhere makes
exceptions extremely problematic. Some of the issues we cause with throw
:
- Unpredictability: Any function call can nuke the entire program.
- Inability to recover: Once an exception is thrown and not handled with a
catch
, there is no easy way to recover. - Verbose syntax: Exception handling requires a lot of boilerplate code, which makes it harder to read and understand.
- Impurity: Functions that
throw
have side effects, making them impure and making functional development with them more complex than it needs to be. - Breaking control flow: Exceptions break the normal control flow of the program, making it harder to reason about.
Why are they so popular, then?
I think there are two reasons for them being so prevalent in JavaScript. On one
side, throw
is very common in class-based languages such as Java, so folks
default to exceptions for all kinds of simple errors. On the other side, the
introduction of async
/await
in JavaScript makes promises easier to reason
about with a syntax that looks synchronous but also turns .then.catch
in
try/catch
blocks.
Don’t get me wrong. The problem is not exceptions. The problem is their overuse. Sometimes developers use them for things that are not exceptional, like checking if a property exists or a value is valid. The code turns into a minefield:
“If something doesn’t go as I want, detonate the entire app.”
What can we use instead?
Fortunately, JavaScript offers us some alternatives to using exceptions that can address some of these issues and provide us with more flexibility and clarity in our code. Here are some examples:
- Use conditional statements: Instead of throwing an exception when
encountering an invalid input or state, we can use conditional statements,
ternary operators, default values, and
undefined
to check for errors and handle them accordingly. This way, we avoid creating unnecessary objects, keep our control flow explicit and avoid breaking composition with other functions:
// Instead of this:const greet = ({ firstName, lastName }) => { if (firstName === undefined || lastName === undefined) { throw new Error("Invalid user"); } return `Hello, ${firstName} ${lastName}`;};
// We can do this:const greet = ({ firstName, lastName }) => firstName !== undefined && lastName !== undefined ? `Hello, ${firstName} ${lastName}`; : "Invalid user"
// Or this:const greet = ({ firstName = "Guest", lastName = "User" }) => `Hello, ${firstName} ${lastName}`;
// Or even this, returning `undefined` so it can be "error handled" with `??` by the consumerconst greet = ({ firstName, lastName }) => firstName !== undefined && lastName !== undefined ? `Hello, ${firstName} ${lastName}`; : undefined
- Use optional chaining and nullish coalescing operators: Instead of
throwing an exception when accessing a property or method that may not
exist, we can use optional chaining (
?.
) and nullish coalescing (??
) operators to safely access nested properties and provide default values if they areundefined
ornull
. This way, we avoid potential type errors, simplify our syntax and avoid breaking composition.
// Instead of this:const getUserName = user => { if ( user !== undefined && user.profile !== undefined && user.profile.name !== undefined ) { return user.profile.name; } throw new Error("Invalid user");};
// We can do this:const getUserName = user => user?.profile?.name ?? "Guest";
// Or this, returning `undefined` so it can be "error handled" with `??` by the consumerconst getUserName = user => user?.profile?.name;
Closing thoughts
This article shows why we don’t need throw
in JavaScript and what we can use
instead. Of course, this does not mean we should never use exceptions. Some
cases still exist where exceptions are appropriate and valuable, such as when
dealing with critical errors that cannot be handled locally or when implementing
custom error types. However, we should be careful and mindful when using
exceptions and avoid abusing them for purposes they are not designed for.
We can write more composable, readable, modular, and robust code by using alternatives such as conditional statements, optional chaining, and nullish coalescing operators. We can also avoid common pitfalls and bad practices leading to bugs or confusion.
So next time you feel tempted to throw
an exception in JavaScript, think twice
and ask yourself: Do I want to make the consumer app explode if this fails? I
promise you, almost always, the answer will be no.