Jump to content

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.

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 consumer
const 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 are undefined or null. 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 consumer
const 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.