This tutorial dives into JavaScript error handling so you’ll be able to throw, detect, and handle your own errors.

Contents:

  1. Showing an Error Message is the Last Resort
  2. How JavaScript Processes Errors
  3. Catching Exceptions
  4. Standard JavaScript Error Types
  5. AggregateError
  6. Throwing Our Own Exceptions
  7. Asynchronous Function Errors
  8. Promise-based Errors
  9. Exceptional Exception Handling

Expert developers expect the unexpected. If something can go wrong, it will go wrong — typically, the moment the first user accesses your new web system.

We can avoid some web application errors like so:

  • A good editor or linter can catch syntax errors.
  • Good validation can catch user input errors.
  • Robust test processes can spot logic errors.

Yet errors remain. Browsers may fail or not support an API we’re using. Servers can fail or take too long to respond. Network connectivity can fail or become unreliable. Issues may be temporary, but we can’t code our way around such problems. However, we can anticipate problems, take remedial actions, and make our application more resilient.

Showing an Error Message is the Last Resort

Ideally, users should never see error messages.

We may be able to ignore minor issues, such as a decorative image failing to load. We could address more serious problems such as Ajax data-save failures by storing data locally and uploading later. An error only becomes necessary when the user is at risk of losing data — presuming they can do something about it.

It’s therefore necessary to catch errors as they occur and determine the best action. Raising and catching errors in a JavaScript application can be daunting at first, but it’s possibly easier than you expect.

How JavaScript Processes Errors

When a JavaScript statement results in an error, it’s said to throw an exception. JavaScript creates and throws an Error object describing the error. We can see this in action in this CodePen demo. If we set the decimal places to a negative number, we’ll see an error message in the console at the bottom. (Note that we’re not embedding the CodePens in this tutorial, because you need to be able to see the console output for them to make sense.)

The result won’t update, and we’ll see a RangeError message in the console. The following function throws the error when dp is negative:


function divide(v1, v2, dp) { return (v1 / v2).toFixed(dp); }

After throwing the error, the JavaScript interpreter checks for exception handling code. None is present in the divide() function, so it checks the calling function:


function showResult() { result.value = divide( parseFloat(num1.value), parseFloat(num2.value), parseFloat(dp.value) ); }

The interpreter repeats the process for every function on the call stack until one of these things happens:

  • it finds an exception handler
  • it reaches the top level of code (which causes the program to terminate and show an error in the console, as demonstrated in the CodePen example above)

Catching Exceptions

We can add an exception handler to the divide() function with a try…catch block:


function divide(v1, v2, dp) { try { return (v1 / v2).toFixed(dp); } catch(e) { console.log(` error name : ${ e.name } error message: ${ e.message } `); return 'ERROR'; }
}

This executes the code in the try {} block but, when an exception occurs, the catch {} block executes and receives the thrown error object. As before, try setting the decimal places to a negative number in this CodePen demo.

The result now shows ERROR. The console shows the error name and message, but this is output by the console.log statement and doesn’t terminate the program.

Note: this demonstration of a try...catch block is overkill for a basic function such as divide(). It’s simpler to ensure dp is zero or higher, as we’ll see below.

We can define an optional finally {} block if we require code to run when either the try or catch code executes:

function divide(v1, v2, dp) { try { return (v1 / v2).toFixed(dp); } catch(e) { return 'ERROR'; } finally { console.log('done'); }
}

The console outputs "done", whether the calculation succeeds or raises an error. A finally block typically executes actions which we’d otherwise need to repeat in both the try and the catch block — such as cancelling an API call or closing a database connection.

A try block requires either a catch block, a finally block, or both. Note that, when a finally block contains a return statement, that value becomes the return value for the whole function; other return statements in try or catch blocks are ignored.

Nested Exception Handlers

What happens if we add an exception handler to the calling showResult() function?


function showResult() { try { result.value = divide( parseFloat(num1.value), parseFloat(num2.value), parseFloat(dp.value) ); } catch(e) { result.value = 'FAIL!'; } }

The answer is … nothing! This catch block is never reached, because the catch block in the divide() function handles the error.

However, we could programmatically throw a new Error object in divide() and optionally pass the original error in a cause property of the second argument:

function divide(v1, v2, dp) { try { return (v1 / v2).toFixed(dp); } catch(e) { throw new Error('ERROR', { cause: e }); }
}

This will trigger the catch block in the calling function:


function showResult() { try { } catch(e) { console.log( e.message ); console.log( e.cause.name ); result.value = 'FAIL!'; } }

Standard JavaScript Error Types

When an exception occurs, JavaScript creates and throws an object describing the error using one of the following types.

SyntaxError

An error thrown by syntactically invalid code such as a missing bracket:

if condition) { console.log('condition is true');
}

Note: languages such as C++ and Java report syntax errors during compilation. JavaScript is an interpreted language, so syntax errors aren’t identified until the code runs. Any good code editor or linter can spot syntax errors before we attempt to run code.

ReferenceError

An error thrown when accessing a non-existent variable:

function inc() { value++; }

Again, good code editors and linters can spot these issues.

TypeError

An error thrown when a value isn’t of an expected type, such as calling a non-existent object method:

const obj = {};
obj.missingMethod(); 

RangeError

An error thrown when a value isn’t in the set or range of allowed values. The toFixed() method used above generates this error, because it expects a value typically between 0 and 100:

const n = 123.456;
console.log( n.toFixed(-1) ); 

URIError

An error thrown by URI-handling functions such as encodeURI() and decodeURI() when they encounter malformed URIs:

const u = decodeURIComponent('%'); 

EvalError

An error thrown when passing a string containing invalid JavaScript code to the eval() function:

eval('console.logg x;'); 

Note: please don’t use eval()! Executing arbitrary code contained in a string possibly constructed from user input is far too dangerous!

AggregateError

An error thrown when several errors are wrapped in a single error. This is typically raised when calling an operation such as Promise.all(), which returns results from any number of promises.

InternalError

A non-standard (Firefox only) error thrown when an error occurs internally in the JavaScript engine. It’s typically the result of something taking too much memory, such as a large array or “too much recursion”.

Error

Finally, there is a generic Error object which is most often used when implementing our own exceptions … which we’ll cover next.

Throwing Our Own Exceptions

We can throw our own exceptions when an error occurs — or should occur. For example:

  • our function isn’t passed valid parameters
  • an Ajax request fails to return expected data
  • a DOM update fails because a node doesn’t exist

The throw statement actually accepts any value or object. For example:

throw 'A simple error string';
throw 42;
throw true;
throw { message: 'An error', name: 'MyError' };

Exceptions are thrown to every function on the call stack until they’re intercepted by an exception (catch) handler. More practically, however, we’ll want to create and throw an Error object so they act identically to standard errors thrown by JavaScript.

We can create a generic Error object by passing an optional message to the constructor:

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

We can also use Error like a function without new. It returns an Error object identical to that above:

throw Error('An error has occurred');

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

throw new Error('An error has occurred', 'script.js', 99);

This is rarely necessary, since they default to the file and line where we threw the Error object. (They’re also difficult to maintain as our files change!)

We can define generic Error objects, but we should use a standard Error type when possible. For example:

throw new RangeError('Decimal places must be 0 or greater');

All Error objects have the following properties, which we can examine in a catch block:

  • .name: the name of the Error type — such as Error or RangeError
  • .message: the error message

The following non-standard 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 listing the function calls made before the error occurred

We can change the divide() function to throw a RangeError when the number of decimal places isn’t a number, is less than zero, or is greater than eight:


function divide(v1, v2, dp) { if (isNaN(dp) || dp < 0 || dp > 8) { throw new RangeError('Decimal places must be between 0 and 8'); } return (v1 / v2).toFixed(dp);
}

Similarly, we could throw an Error or TypeError when the dividend value isn’t a number to prevent NaN results:

 if (isNaN(v1)) { throw new TypeError('Dividend must be a number'); }

We can also cater for divisors that are non-numeric or zero. JavaScript returns Infinity when dividing by zero, but that could confuse users. Rather than raising a generic Error, we could create a custom DivByZeroError error type:


class DivByZeroError extends Error { constructor(message) { super(message); this.name = 'DivByZeroError'; }
}

Then throw it in the same way:

if (isNaN(v2) || !v2) { throw new DivByZeroError('Divisor must be a non-zero number');
}

Now add a try...catch block to the calling showResult() function. It can receive any Error type and react accordingly — in this case, showing the error message:


function showResult() { try { result.value = divide( parseFloat(num1.value), parseFloat(num2.value), parseFloat(dp.value) ); errmsg.textContent = ''; } catch (e) { result.value = 'ERROR'; errmsg.textContent = e.message; console.log( e.name ); } }

Try entering invalid non-numeric, zero, and negative values into this CodePen demo.

The final version of the divide() function checks all the input values and throws an appropriate Error when necessary:


function divide(v1, v2, dp) { if (isNaN(v1)) { throw new TypeError('Dividend must be a number'); } if (isNaN(v2) || !v2) { throw new DivByZeroError('Divisor must be a non-zero number'); } if (isNaN(dp) || dp < 0 || dp > 8) { throw new RangeError('Decimal places must be between 0 and 8'); } return (v1 / v2).toFixed(dp);
}

It’s no longer necessary to place a try...catch block around the final return, since it should never generate an error. If one did occur, JavaScript would generate its own error and have it handled by the catch block in showResult().

Asynchronous Function Errors

We can’t catch exceptions thrown by callback-based asynchronous functions, because an error is thrown after the try...catch block completes execution. This code looks correct, but the catch block will never execute and the console displays an Uncaught Error message after one second:

function asyncError(delay = 1000) { setTimeout(() => { throw new Error('I am never caught!'); }, delay); } try { asyncError();
}
catch(e) { console.error('This will never run');
}

The convention presumed in most frameworks and server runtimes such as Node.js is to return an error as the first parameter to a callback function. That won’t raise an exception, although we could manually throw an Error if necessary:

function asyncError(delay = 1000, callback) { setTimeout(() => { callback('This is an error message'); }, delay); } asyncError(1000, e => { if (e) { throw new Error(`error: ${ e }`); } });

Promise-based Errors

Callbacks can become unwieldy, so it’s preferable to use promises when writing asynchronous code. When an error occurs, the promise’s reject() method can return a new Error object or any other value:

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); } }) }

Note: functions must be either 100% synchronous or 100% asynchronous. This is why it’s necessary to check the delay value inside the returned promise. If we checked the delay value and threw an error before returning the promise, the function would become synchronous when an error occurred.

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


wait('INVALID') .then( res => console.log( res )) .catch( e => console.error( e.message ) ) .finally( () => console.log('complete') );

Personally, I find promise chains a little difficult to read. Fortunately, we can use await to call any function which returns a promise. This must occur inside an async function, but we can capture errors using a standard try...catch block.

The following (immediately invoked) async function is functionally identical to the promise chain above:

(async () => { try { console.log( await wait('INVALID') ); } catch (e) { console.error( e.message ); } finally { console.log('complete'); } })();

Exceptional Exception Handling

Throwing Error objects and handling exceptions is easy in JavaScript:

try { throw new Error('I am an error!');
}
catch (e) { console.log(`error ${ e.message }`)
}

Building a resilient application that reacts appropriately to errors and makes life easy for users is more challenging. Always expect the unexpected.

Further information:

Similar Posts