An Introduction to JavaScript Error Handling


Error handling is simple in JavaScript but it’s often shrouded in mystery and can become complicated when considering asynchronous code.

An “error” is a object you can throw to raise an exception — which could halt the program if it’s not captured and dealt with appropriately. You can create an Error object by passing an optional message to the constructor:

const e = new Error('An error has occurred');

You can also use Error like a function without new — it still returns an Error object identical to that above:

const e = Error('An error has occurred');

You can pass a filename and a line number as the second and third parameters:

const e = new Error('An error has occurred', 'script.js', 99);

This is rarely necessary since these default to the line where you created the Error object in the current file.

Once created, an Error object has the following properties which you can read and write:

  • .name: the name of the Error type ("Error" in this case)
  • .message: the error message

The following read/write properties are also supported in Firefox:

  • .fileName: the file where the error occurred
  • .lineNumber: the line number where the error occurred
  • .columnNumber: the column number on the line where the error occurred
  • .stack: a stack trace — the list of functions calls made to reach the error.

Error Types

As well as a generic Error, JavaScript supports specific error types:

The JavaScript interpreter will raise appropriate errors as necessary. In most cases, you will use Error or perhaps TypeError in your own code.

Throwing an Exception

Creating an Error object does nothing on its own. You must throw an Error to raise an exception:

throw new Error('An error has occurred');

This sum() function throws a TypeError when either argument is not a number — the return is never executed:

function sum(a, b) {

  if (isNaN(a) || isNaN(b)) {
    throw new TypeError('Value is not a number.');
  }

  return a + b;

}

It’s practical to throw an Error object but you can use any value or object:

throw 'Error string';
throw 42;
throw true;
throw { message: 'An error', name: 'CustomError' };

When you throw an exception it bubbles up the call stack — unless it’s caught. Uncaught exceptions eventually reach the top of the stack where the program will halt and show an error in the DevTools console, e.g.

Uncaught TypeError: Value is not a number.
  sum https://mysite.com/js/index.js:3

Catching Exceptions

You can catch exceptions in a try ... catch block:

try {
  console.log( sum(1, 'a') );
}
catch (err) {
  console.error( err.message );
}

This executes the code in the try {} block but, when an exception occurs, the catch {} block receives the object returned by the throw.

A catch block can analyse the error and react accordingly, e.g.

try {
  console.log( sum(1, 'a') );
}
catch (err) {
  if (err instanceof TypeError) {
    console.error( 'wrong type' );
  }
  else {
    console.error( err.message );
  }
}

You can define an optional finally {} block if you require code to run whether the try or catch code executes. This can be useful when cleaning up, e.g. to close a database connection in Node.js or Deno:

try {
  console.log( sum(1, 'a') );
}
catch (err) {
  console.error( err.message );
}
finally {
  // this always runs
  console.log( 'program has ended' );
}

A try block requires either a catch block, a finally block, or both. Note that when a finally block contains a return, that value becomes the return value for the whole try ... catch ... finally regardless of any return statements in the try and catch blocks.

Nested try ... catch Blocks and Rethrowing Errors

An exception bubbles up the stack and is caught once only by the nearest catch block, e.g.

try {

  try {
    console.log( sum(1, 'a') );
  }
  catch (err) {
    console.error('This error will trigger', err.message);
  }

}
catch (err) {
  console.error('This error will not trigger', err.message);
}

Any catch block can throw a new exception which is caught by the outer catch. You can pass the first Error object to a new Error in the cause property of an object passed to the constructor. This makes it possible to raise and examine a chain of errors.

Both catch blocks execute in this example because the first error throws a second:

try {

  try {
    console.log( sum(1, 'a') );
  }
  catch (err) {
    console.error('First error caught', err.message);
    throw new Error('Second error', { cause: err });
  }

}
catch (err) {
  console.error('Second error caught', err.message);
  console.error('Error cause:', err.cause.message);
}

Throwing Exceptions in Asynchronous Functions

You cannot catch an exception thrown by an asynchronous function because it’s raised after the try ... catch block has completed execution. This will fail:

function wait(delay = 1000) {

  setTimeout(() => {
    throw new Error('I am never caught!');
  }, delay);

}

try {
  wait();
}
catch(err) {
  // this will never run
  console.error('caught!', err.message);
}

After one second has elapsed, the console displays:

Uncaught Error: I am never caught!
  wait http://localhost:8888/:14

If you’re using a callback, the convention presumed in frameworks and runtimes such as Node.js is to return an error as the first parameter to that function. This does not throw an exception although you can manually do that when necessary:

function wait(delay = 1000, callback) {

  setTimeout(() => {
    callback('I am caught!');
  }, delay);

}

wait(1000, (err) => {

  if (err) {
    throw new Error(err);
  }

});

In modern ES6 it’s often better to return a Promise when defining asynchronous functions. When an error occurs, the Promise’s reject method can return a new Error object (although any value or object is possible):

function wait(delay = 1000) {

  return new Promise((resolve, reject) => {

    if (isNaN(delay) || delay < 0) {
      reject(new TypeError('Invalid delay'));
    }
    else {
      setTimeout(() => {
        resolve(`waited ${ delay } ms`);
      }, delay);
    }

  })

}

The Promise.catch() method executes when passing an invalid delay parameter so it can react to the returned Error object:

// this fails - the catch block is run
wait('x')
  .then( res => console.log( res ))
  .catch( err => console.error( err.message ))
  .finally(() => console.log('done'));

Any function which returns a Promise can be called by an async function using an await statement. You can contain this in a try ... catch block which runs identically to the .then/.catch Promise example above but can be a little easier to read:

// Immediately-Invoked (Asynchronous) Function Expression
(async () => {

  try {
    console.log( await wait('x') );
  }
  catch (err) {
    console.error( err.message );
  }
  finally {
    console.log('done');
  }

})();

Errors are Inevitable

Creating error objects and raising exceptions is easy in JavaScript. Reacting appropriately and building resilient applications is somewhat more difficult! The best advice: expect the unexpected and deal with errors as soon as possible.