- Key Takeaways
- Introduction
- 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
- Frequently Asked Questions (FAQs) on JavaScript Error Handling
This tutorial dives into JavaScript error handling so you’ll be able to throw, detect, and handle your own errors.
- 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
Key Takeaways
- Comprehensive Guide on JavaScript Error Handling: The article serves as an in-depth tutorial on managing JavaScript errors effectively, covering topics from anticipating and mitigating errors to implementing custom error handling strategies for a more resilient web application.
- Practical Techniques and Examples: It provides practical techniques for throwing, detecting, and handling errors in JavaScript, including working with standard error types, managing asynchronous function errors, and leveraging promises for better error management.
- Emphasis on Proactive Error Management: The tutorial emphasizes the importance of proactive error management in JavaScript, advocating for anticipating potential errors and implementing robust error handling mechanisms to enhance user experience and application reliability.
Introduction
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:
// division calculation
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:
// show result of division
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:
// division calculation
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?
// show result of division
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:
// show result of division
function showResult() {
try {
//...
}
catch(e) {
console.log( e.message ); // ERROR
console.log( e.cause.name ); // RangeError
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) { // SyntaxError
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++; // ReferenceError
}
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(); // TypeError
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) ); // RangeError
URIError
An error thrown by URI-handling functions such as encodeURI() and decodeURI() when they encounter malformed URIs:
const u = decodeURIComponent('%'); // URIError
EvalError
An error thrown when passing a string containing invalid JavaScript code to the eval() function:
eval('console.logg x;'); // EvalError
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:
// division calculation
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:
// new 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:
// show result of division
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:
// division calculation
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:
// invalid delay value passed
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:
Frequently Asked Questions (FAQs) on JavaScript Error Handling
What are the different types of errors in JavaScript?
JavaScript has three types of errors: Syntax Errors, Runtime Errors, and Logical Errors. Syntax Errors occur when there is an issue with the structure of your code, such as a missing bracket or semicolon. Runtime Errors happen when the code is syntactically correct but fails to execute due to unforeseen circumstances like referencing an undefined variable. Logical Errors are the most challenging to debug as they occur when the code runs without crashing, but it doesn’t produce the expected outcome.
How can I handle errors in JavaScript?
JavaScript provides several mechanisms for error handling, including try-catch-finally blocks, throwing exceptions, and using error events. The try-catch-finally block is the most common method. It allows you to “try” a block of code and “catch” any errors that occur. The “finally” block will execute regardless of whether an error was thrown.
What is the purpose of the ‘throw’ statement in JavaScript?
The ‘throw’ statement allows you to create custom error messages in JavaScript. It’s useful when you want to generate an error that the JavaScript engine wouldn’t typically produce. For example, you might want to throw an error if a function argument is not of the expected type.
How can I create a custom error in JavaScript?
You can create a custom error in JavaScript by defining a new object that inherits from the Error constructor. This new object can then be thrown using the ‘throw’ statement. This is useful when you want to provide more specific error information than the standard Error types provide.
What is the difference between ‘null’ and ‘undefined’ in JavaScript?
In JavaScript, ‘null’ and ‘undefined’ both represent the absence of value. However, they are used in slightly different contexts. ‘Undefined’ means a variable has been declared but has not yet been assigned a value. On the other hand, ‘null’ is an assignment value that represents no value or no object.
How can I debug JavaScript errors?
Debugging JavaScript errors can be done using various tools and techniques. Most modern browsers come with built-in developer tools that include a JavaScript console, which displays errors and allows you to interact with your code. You can also use breakpoints to pause code execution and inspect the current state.
What is ‘strict mode’ in JavaScript and how does it help with error handling?
Strict mode’ is a feature in JavaScript that helps catch common coding mistakes and “unsafe” actions. For example, in strict mode, variables must be declared with ‘var’, ‘let’, or ‘const’. Trying to use an undeclared variable will result in an error. This can help prevent bugs that are difficult to detect.
What is the difference between a ‘TypeError’ and a ‘ReferenceError’ in JavaScript?
A ‘TypeError’ occurs when an operation could not be performed, typically when a value is not of the expected type. A ‘ReferenceError’ is thrown when trying to dereference a variable that has not been declared.
How can I handle asynchronous errors in JavaScript?
Asynchronous errors can be handled using promises or async/await. Promises have ‘catch’ and ‘finally’ methods similar to try-catch-finally blocks. With async/await, you can use a try-catch block to handle errors in asynchronous code.
What is the ‘onerror’ event handler in JavaScript?
The ‘onerror’ event handler is a global event handler in JavaScript that catches errors that occur in the window context. It can be used to log uncaught exceptions or to handle them in a custom way.
Craig is a freelance UK web consultant who built his first page for IE2.0 in 1995. Since that time he's been advocating standards, accessibility, and best-practice HTML5 techniques. He's created enterprise specifications, websites and online applications for companies and organisations including the UK Parliament, the European Parliament, the Department of Energy & Climate Change, Microsoft, and more. He's written more than 1,000 articles for SitePoint and you can find him @craigbuckler.