Exception Handling in JavaScript Made Easy

Welcome to the wild world of Exception Handling, where programmers write code that tries to do the right thing and catches itself in the act of doing something else. That’s going to make sense in a few minutes. Trust me.

1. What is Exception Handling?

Exception handling can be a daunting subject, especially to inexperienced developers. Since I don’t know where you’re at in your journey let’s forget about programming for just a minute. In the English language, what does the word exception mean?

Something excepted; an instance or case not conforming to the general rule.

(Exception Definition & Meaning, Dictionary.com)

So that means an exception is something out of the ordinary. On the negative side of the spectrum, that kind of sounds like an error, doesn’t it?

We can start from a simple, high-level premise: an Exception is just a kind of error, and exception handling is how we deal with them.

2. Exception Handling with the The Try…Catch Statements

Most programming languages that support exception handling do so with a very similar structure: The trycatch statements, and JavaScript is no different.

try {
    // Do some work
} catch (error) {
    // Handle errors
}

You’ll notice there are two code blocks: a try block and a catch block. One cannot exist without the other: a try { … } must always be followed by a catch { … }. (Note: though this is the most common pattern, technically try can also be followed by finally, but we’ll get to that.)

The Try Block

The try { ... } block tries to run the code within it. If an exception is thrown during execution, the code that follows is not executed.

(Note the use of the verb throw. When an exception occurs, we say the code threw an exception. You may also hear the verb raising an exception too. Throwing an exception and raising an exception mean the same thing.)

There is very little more to say about the try block:

  • The keyword try followed by a bracketed block of code.
  • The code in the block tries to execute. If an exception is thrown, the code after the exception is thrown is not executed.
  • Must be followed by a catch { ... } block, a finally { ... } block, or both.

The Catch Block

A catch block handles Exceptions thrown from the try block that precedes it. It begins with the catch keyword, optionally followed by (error), where error defines the name of a variable that will reference the Exception, followed by a block { ... } containing the exception handling code.

There are a few things to know about the catch statement:

  • It has two forms. You can simply write catch { ... } if you don’t care about the error’s details in order to handle it, or you can write catch (error) { ... } where error is a variable that references the caught Exception.
  • Don’t catch Exceptions you can’t handle. You should only use try...catch to catch errors you can reasonably handle. If you can’t handle the error, then don’t catch it. Let it “bubble up.”
  • Write Tight, Limited try...catch blocks. You should only wrap code in try...catch for which you are prepared to catch and handle Exceptions. Avoid wrapping entire method bodies in a single try...catch. If you do, you risk handling/squashing exceptions you don’t actually want to catch.
  • Re-throw Exceptions you don’t know how to handle. If multiple kinds of Exceptions are possible, inspect the Exception by type (using instanceof), or by name (error.name === 'DomainError').

Now that we have try and catch, we know just enough syntax to implement exception handling in our code. Here’s a simple example:

/**
 * The following code will throw and catch a RangeError by trying to create an Array with an
 * invalid range.
 */
try {
    // Try to create a new array with an invalid length.
    const array = new Array(-1);
} catch (error) {
    // Check for the kind of error we want to handle.
    if (error instanceof RangeError) {
        // Handle the error by reporting a more useful error.
        console.error('An array cannot have a length less than zero.');
    } else {
        // Rethrow any errors we don't actually know how to handle.
        throw error;
    }
}

The Finally Block

A finally block provides code that runs after the try and catch phases, regardless of whether an error happened or not. It can be used to perform steps that should always happen regardless of the success of the code in the try or whether an error was throw or caught.

A practical use case is in a graphical application when you want to show a progress indicator or spinner while a process runs, and hide the spinner when it completes regardless of success or failure:

/**
 * The following code shows a spinner, performs a long-running calculation, and then hides
 * the spinner when the process completed regardless of whether or not an Exception was
 * thrown.
 */

// Show the spinner. Assume this function is defined.
showSpinner();

try {
    // Assume generateReport() exists, does a bunch of complicated work, and could
    // throw an Exception if something goes wrong.
    generateReport();
} catch(error) {
    // ... handle the exception  ...
} finally {
    // Hide the spinner after try and catch complete.
    hideSpinner();
}
    

Note: While less relevant in front-end JavaScript code, finally is also very useful for closing resources when a process completes, regardless of success. Think open file pointers, database connections, etc.

3. The Error Class

In JavaScript the Error class is the base for all runtime errors and one of the fundemental building blocks of exception handling. There are a number of sub types of Error including EvalError, RangeError, ReferenceError, and SyntaxError.

You can also extend Error to create our own custom error types. For example, we might use DomainError when we want to throw an Exception caused by a violation of our domain logic. Extending Error looks like this:

/**
 * A DomainError represents an error caused by a fault in business logic.
 */
class DomainError extends Error {
    /**
     * Creates a new DomainError.
     *
     * @param {string} message The error message.
     * @param {object|null} options Options that specify the cause.
     */
    constructor(message, options) {
        // Call the base Error class constructor
        super(message, options);

        // Set the name of our Error type
        this.name = 'DomainError';
    }
}

The Error class has several standard properties, set via it’s constructor:

  • message – The message that describes why the error occurred.
  • options – An optional object which can specify the cause or the error. (You won’t use this much.)

Depending on the browser Error also supports a bunch of nonstandard properties, which you can learn about here.

Now you know how to create an exception. Creating an Exception doesn’t do anything. You need to throw it. To throw it, use the throw keyword:

const age = parseInt('userInput', 10);

if (isNaN(age)) {
    throw new DomainException('age was not a number, which violates our business rules');
}

Throwing an exception breaks the flow of execution. Any code that follows the throw statement will not be executed. The exception “bubbles up” through the call stack to the closest catch { ... } block, which will handle the Exception. If the exception isn’t caught, the JavaScript engine will handle it by sending the error to the console.

One other important caveat of JavaScript: you can throw literally anything. It doesn’t have to be an Error. But I don’t recommend it!

/**
 * In the code below we get user input and convert it to a number. If the user input is not
 * a valid number we throw a string as an error.
 */

try {
    // Assume <input id="age"/> exists and we get it's value as a string.
    const ageInput = document.querySelector('#age').value;

    // Parse the input into a base-10 number
    const age = parseInt(ageInput, 10);
    
    // Use isNaN() to check if the user entered an invalid number.
    if (isNaN(age)) {
        // Look what we're doing: throwing a string instead of an Error. Very legal and very cool
        throw `Some lunatic thinks ${ageInput} is a valid age. Enter a number, you goofball.`;
    }
catch (error) {
    // When an Exception is caught, log an message that tells us the type. It should be "string"
    console.log('An error occurred and it's type was ' + (typeof error));
}

Let’s review:

  • Error is the base type for all errors in JavaScript.
  • Several subclasses of Error already exist in JavaScript including EvalError, RangeError, ReferenceError, and SyntaxError.
  • We can extend Error to create our own custom error types.
  • Error has two properties: message and options. Both are optional.
  • We can throw anything in JavaScript. But we should throw only Error and Error subtypes.

4. When Do Exceptions Get Thrown?

Exceptions get thrown under the following conditions:

  • You throw an Exception from your own Userland code, using the throw statement.
  • JavaScript throws an exception from an internal function or as a result of some condition your Userland code caused, such as a TypeError, SyntaxError, RangeError, etc.

All errors in JavaScript are thrown as exceptions. When you see an error reported in your browser console, that’s an exception being thrown and eventually being handled by the browser. For example, any of the following simple JavaScript statements throw exceptions that would get reported in the browser console:

// Throws a SyntaxError
JSON.parse('this is invalid json');

// Throws a TypeError because book.metadata is not defined.
const book = { title: 'The Catcher in the Rye' };
const id = book.metadata.id;

If you’ve seen an error reported in your console, you’ve already seen Exceptions in action. See? You’re further along than you realized!

6. Exceptions in Asynchronous Code (Promises)

The try...catch...finalize structure is synchronous by nature. This means that try...catch cannot catch and handle an Exception thrown by an asynchronous function calls such fetch(). How would you handle a failure in when calling an API endpoint?

JavaScript has you covered. Asynchronous functions return Promises, and Promises support exceptions via the Promise.prototype.catch() and Promise.prototype.finally() methods.

/**
 * In this hypothetical code block, we first display a spinner to show the application is
 * working.
 *
 * We load a user record from a REST API. If the request fails we throw an exception
 * which we'll catch later. If the request succeeds we convert the body to JSON. If conversion
 * fails it will throw an Exception that we'll catch later. If conversion to JSON succeeds, we 
 * send the user record to hypothetical controller component to handle it.
 *
 * If the request fails or returns invalid JSON, the thrown Exception is caught, logged, and an
 * error is displayed to the user.
 *
 * After the request completes either success or failure are handled, we hide the spinner via
 * finally.
 */

// Show the spinner
showSpinner();

// Make REST API request
fetch(`/user/${id}`)
    .then((response) => {
        // If we don't get an OK response, throw an Exception. fetch does not do this on it's own
        if (! response.ok) {
            throw new Error('API request failed.');
        }
 
        // Convert body to JSON. Will throw an Exception if not valid JSON
        return response.json();
    })
    .then(user => {
        // Set the user, which will update the view.
        this.getController().setUser(user);
    })
    .catch(error => {
        // Log the failure
        this.getLogger().error(error);

        // Show an error to the user.
        this.getController().showError({
            title: 'API Request Failed',
            message: 'Failed to load data from the backend'   
    })
    .finally(() => {
        // Turn off the spinner.
        hideSpinner()
    });

Summary

Congratulations. Now you know how to catch, handle, and throw Exceptions in JavaScript code. You know how to handle Exceptions in asynchronous code via the catch and finally functions built into Promises. In a future article, I’ll be teaching you how Exceptions work in PHP, were things get even more interesting.

Why My Team Chose Web Components Over React

My team recently backed out of a decision to adopt React as the foundation of a product rewrite and chose Web Components instead. This post explains why.

TL;DR

To sum up the situation: my organization had decided that we would do a rewrite of our application. Think Basecamp’s “The Big Rewrite.” Only without Basecamp’s resources.

While words of support for a rewrite were vocalized, the reality that the organization couldn’t live with the feature freeze required to write a new app set in. And so, we change strategies to focus on iterating what we have. But that required us to reconsider key technical decisions that supported the rewrite. Should we still adopt React?

Grafting React onto our legacy codebase is infeasible. Reaching our goals of modernizing our UI and modularizing our code required us to limit consideration to solutions we could use today, with our existing codebase. The obvious choice was Web Components.

Why We Decided Not to Adopt React

I’d like to tell you we’re adopting Web Components because we did some deep, data-driven research that supports the decision. We didn’t.

I’d like to say the Web Component API is so mature and so universally adored that it has knocked React from it’s throne .It’s not, and it hasn’t, and it probably won’t.

More than anything else, I’d like to tell you we made a brilliant decision and it was my expert leadership that got us there. It did not, and there’s a nonzero chance I’m an idiot and you shouldn’t even be reading this. You’ve been warned!

In reality once we reached consensus that a rewrite wasn’t realistic, there was very little decision making to be done. Here’s why.

Our Current Technical Realities

Our existing application is Vanilla JavaScript written and bundled in in a specific order such that dependencies are resolved by order of parsing the code, not any modern concepts like imports, modules, etc.

We could overcome that with a few weeks of refactoring. But then we have to contend with the way the frontend code was originally written, which is to say setup camp in the innermost circle of global scope hell.

Modern tooling, bundlers, frameworks, have expectations of code being well-organized. Ours is not. And as previously discussed, we can’t spend a year or so rewriting it from scratch.

Given the shape of our codebase, attempting to graft on React would be difficult and painful. We would have to fundamentally reshape our application to conform to React’s expectations. And since we’ve already concluded that we don’t have the time or resources for that, React adoption was a non-starter.

Given this new shared reality, what could we do?

Start By Defining the Problem

Let’s face it: developers like React, and it can be easy to have a preferred solution in-hand and try to work backwards from there. In this instance we caught ourselves in the act and reacted accordingly.

We had to start over by redefining our problems and limitations.

What Problem Are We Trying To Solve?

  • We want to adopt a Component Driven Development philosophy and ship modular, reusable UI components.
  • We want to build accessible, attractive, consistent, responsive, and reusable components from which we can compose complex views in our application in the future.
  • Any solution to those problems must “play nice” with our existing codebase so we can introduce our components as evolution, not a single revolutionary rewrite.

Web Components Solve Our Problem

Web Components are not popular. They’re not easy. And they certainly don’t solve all the problems. But they have a lot going for them which make them the perfect solution for us:

  • They’re here. Web components are a native web API that is now well supported in all major browsers.
  • They’re stable. They’re a native web API, which means we can start using them today without worrying about npm dependency hell making life problematic for us in the future.
  • We can use them today. Because they are a native API, and because once you register a web component you simply use it as an HTML tag, we can introduce web components to our codebase today without any major rewrites, refactoring, or changes to our build process.
  • Web Components Help Write Modular Frontend Code. Component is right there in the name. Writing components that implement a generic user interface feature will support our desire to write modular code that separates the view from storage and logic going forward.
  • They Don’t Preclude Using a Framework in the Future. Major frameworks like Angular, Vue, and React are all figuring out how to coexist and support web components. If we reach a point where we can adopt a framework in the future, we don’t have to throw away our work.