Error Handling in JavaScript: a Quick Guide

Is There a Right Way to Get Stuff Wrong?

Bret Cameron
Oct 10 · 9 min read

There is no such thing as perfect code. Even if there was, our users will always find surprising ways to break what once seemed like perfectly watertight code and attempt to do things that we don’t want them to! How do we control these perilous situations? Using errors!

In this article, we’ll look into error handling in JavaScript. We’re specifically talking about runtime errors — sometimes called execution errors. These are the errors that occur while our programming is running: if they’re not caught, then they’ll typically crash our programs, something we usually want to avoid! By the end of the article, you’ll have seen several different approaches to error handling in JavaScript, as well as several ways to extend JavaScript’s built-in Error to make it more useful.

But first, a thorny question…

Should I use throw, return or console.error?

Even if you’re new to JavaScript, you should have a decent understanding of return . You may not have encountered console.error or console.exception, but these are also easy to grasp: they’re essentially console.log — except, in eye-popping red!

But what about the throw statement? This is the keyword used to throw an exception, an event which disrupts the normal flow of our program’s instructions. And this is the key characteristic which distinguishes throw from return: after a function throws an exception, the runtime system will try to find a way to handle the exception. In other words, throw will look additional actions to take. By contrast, return will simply finish the execution of a function.

Using throw without a try ... catch statement

function getRectArea(width, height) {
if (isNaN(width) || isNaN(height)) {
throw new Error("Parameter is not a number!");
};
return width * height;
};
console.log(getRectArea('abc', 123));

If we run the code above, the funciton will output the same thing, regardless of whether we throw or return the error.

There is a difference, though. If there is no catch statement, throw will log to the console by default and it will prevent our program from continuing execution. If we use return , this won’t happen, and we have to specifically call console.log to see whatever our function has returned.

Using throw with a try ... catch statement

function getRectArea(width, height) {
if (isNaN(width) || isNaN(height)) {
throw new Error("Parameter is not a number!");
};
return width * height;
};
try {
getRectArea('abc', 123);
} catch (e) {
console.error(e);
};

In this example, only throw will trigger the catch block. (We cannot replace is with return and get the same logic to execute). If there is no catch block, throw will stop the program and log the error to the console.

So, when should we use throw ? If an error is not expected in normal operation (that is to say, it occurs rarely), then that is the best time to reach for throw . It is almost always the easiest — and, sometimes, the fastest — way to catch rare exceptions, because the program will immediately execute the relevant catch statement.

Common exceptions: a case when throw might not be unnecessary

Later in the article, we’ll create a new error type called RequiredInputError . This will help other developers working on our project understand and locate the error, but — as this error could be quite common — we don’t want to unnecessarily slow our users’ experience by immediately reaching for throw .

Solution 1

function isBlank(input) {
if (input) return false;
console.error(new Error('A required field is blank.'));
return true;
}

Solution 2

function isBlank(input) {
if (input) return false;
return new Error('A required field is blank.');
};
const input = "";if (isBlank(input) instanceof Error) {
console.error(isBlank(input));
};

Returning errors in promises

For Promises, it’s advised to use the reject and resolve methods (which return whatever’s inside the parentheses), and then to use Promise.prototype.catch() for any additional error handling.

Here’s a simple example of that in action — a function which asynchronously checks whether a user’s input is blank and, if it is, returns an Error :

async function isBlankAsync(input) {
return new Promise((resolve, reject) => {
if (!input) reject(new Error('A validation error'));
resolve(input);
});
};
isBlankAsync()
.catch(e => console.error(e));

Understanding the Error object

But, as far as I can tell, there are two reasons to reach for the Error object, rather than just throwing (or returning) a string:

  1. The Error object is the conventional way of describing errors, so this is what libraries, frameworks and other developers expect, and so it makes your code more readable and accessible.
  2. The Error object contains additional information — primarily, the “error stack” — which is really useful when it comes to debugging. The error stack traces which functions were called, in what order, from which line and file, and with what arguments: everything you’re used to seeing below the main error message.

Error properties and methods

const err = new Error("Incorrect file type provided");

By default, our Error has two properties:

  • a name — in this case, "Error"
  • a message — in this case, "Incorrect file type provided"

On top of that, we have the toString method, which prints you the name and the message together. There are a handful of additional properties and methods, but these are mostly specific to Firefox, so they’re not recommended in production!

For the remainder of this article, we’ll look into three ways we can extend the basic Error object into something that’s more useful — at least, in specific situations.

Creating a TypeError Class

Let’s say we wanted a TypeError that, by default, told us which type a variable was supposed to be and which type it actually is. Our constructor should take a value and a type . Then, unless a message is provided, we’ll create a default message that makes any type differences clear. We’ll be using ES6+ classes, and all of the logic can go in the constructor:

class TypeError extends Error {
constructor(value, type, ...params) {
super(...params);

if (Error.captureStackTrace) {
Error.captureStackTrace(this, TypeError);
};
this.name = 'TypeError';
this.typeRequired = type;
this.typeSupplied = typeof value;
this.value = value;
if (!this.message && this.typeRequired !== this.typeSupplied) {
this.message = `The value ${this.value} has type "${this.typeSupplied}", but it should have type "${this.typeRequired}".`;
};
};
};

First, we call super(...params) to pass the default arguments (like message) to the parent Error object.

Next, we check to see whether the Error object has a captureStackTraceMethod . If it does, we should call it, as that will provide our class with a stack property. (Generally, if captureStackTraceMethod , the stack is provided by default).

Finally, we create name , typeRequired and typeSupplied properties. If a message isn’t provided and the typeSupplied is not identical to the typeRequired , we send a default message. That’s it!

Paste in the code above and try calling new TypeError("10", "number") . In some environments, like Node.js, you’ll see the whole error object. To make sure you’re only seeing the stack, call new TypeError("10", "number").stack .

Creating a RequiredInputError Class

What if we wanted to log an error to the console and send a short error message to our end-users? There are lots of ways to achieve, but handling both in a custom Error objects is one way to keep things neat.

First, let’s create a new error class called RequiredInputError :

class RequiredInputError extends Error {
constructor(label, ...params) {
super(...params);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, RequiredInputError);
};
this.name = 'UserInputError';
this.label = label;
this.userMessage = `The field "${label}" is required.`;if (!this.message) this.message = this.userMessage;
};
};

Our new class is similar to our TypeError class above, but we’ve added a new property called userMessage , which we can then display in the DOM.

The exact way you go about this will depend on the framework you’re using, so here’s just one example. Let’s say we had a variable called userInput and we were using React. We could create an error object, send the error’s userMessage to our end-user using state, and — only if we’re in a development environment — log the error stack to the console. Let’s create a function that does exactly that:

const isBlank = (userInput, fieldName) => {
if (!userInput) {
const error = new RequiredInputError(fieldName);
this.setState({ error: error.userMessage });
if (process.env.NODE_ENV !== 'production')
console.error(error.stack);
return true;
};
return false;
};

Now, can simply call isBlank on a variable:

isBlank(userInput, "email");

If the input is blank, the user will see a message that the variable is required. In addition, any developers working on the project will see all the information they need to know about where the error is occurring and why!

This may seem like overkill for just one input, but if your website has a lot of forms, it could be useful to identify missing fields with a unique class of error.

Adding a toJSON method

This is fairly simple to achieve — we just need to expose the various properties in a new method:

class CustomError extends Error {
constructor(...params) {
super(...params);

if (Error.captureStackTrace) {
Error.captureStackTrace(this, CustomError);
};
this.name = 'ValidationError'; this.date = new Date();
}
toJSON() {
return {
name: this.name,
date: this.date,
message: this.message,
stack: this.stack
}
}
}

There are a lot of similarities to the previous two examples. This time, we’ve added a custom date property, and in our toJSON method, we’re returning all the properties in their own object. This makes it easy to send and receive all the data available with each error. It could also be used to preserve the call trace if you want to rethrow an error.

Conclusion

If you’d like to find out more about any of the topics discussed in the article, I would head over to MDN, which has a lot of useful examples. Here are some suggestions to get you started:

If you have any questions or feedback, feel free to leave a comment!

The Startup

Medium's largest active publication, followed by +529K people. Follow to join our community.

Bret Cameron

Written by

Writer and developer based in London. On Medium, I write about JavaScript and web development 💻

The Startup

Medium's largest active publication, followed by +529K people. Follow to join our community.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade