This tutorial dives into JavaScript error handling so you’ll be able to throw, detect, and handle your own errors.
Contents:
- Showing an Error Message is the Last Resort
- How JavaScript Processes Errors
- Catching Exceptions
- Standard JavaScript Error Types
- AggregateError
- Throwing Our Own Exceptions
- Asynchronous Function Errors
- Promise-based Errors
- 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
Table of Contents
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 asError
orRangeError
.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: