Skip to content

Error Handling in JavaScript

So far, the code we’ve written has assumed that everything goes right. Every value is what we expect, every request succeeds, and every user types exactly what we asked for.

But here’s the catch:

Real-world apps don’t work like that. A server goes down. A user types their phone number in the email field. A file you need just… isn’t there.

If your code isn’t ready for it, one bad line can bring the whole app down. Good apps don’t crash when this happens. They handle it.

Let’s explore how.

Suppose you decided to go out for dinner at a restaurant one day, and you ordered a pizza with some very specific toppings. The waiter took your order to the chef. The chef acknowledged the order and started preparing it, and then realized that the toppings you ordered weren’t available.

Now, what would a restaurant usually do in such a situation? Shut down the whole restaurant? Obviously not! They would let you know the toppings aren’t available, and you might decide to order a pizza with some other toppings, or maybe something else entirely.

That’s it. Something went wrong. Somebody dealt with it. Dinner went on.

That’s exactly what error handling brings to JavaScript, and the main tool for it is the try...catch block.

If you remember, I asked a question earlier: “Shut down the whole restaurant?” You might be wondering why I asked that. How could one order going wrong shut down a whole restaurant, and what does that have to do with this article?

Good question! Well, in the real world, a restaurant won’t shut down over one order. But on the internet, if errors aren’t handled properly, a single failed order can crash your whole full-stack e-commerce website.

Error handling is the process of anticipating, detecting and responding to errors in a controlled way, so that your software doesn’t crash and stays reliable.

In JavaScript, we use the try...catch block to handle errors.

try {
    // code you want to run
} catch (error) {
    // runs only if something in the try block throws an error
} finally {
    // (optional) runs no matter whether the try block succeeded or failed
}

In the above example, we have three parts:

  1. The try block contains the code that does what you originally wanted. e.g. Ordering a pizza with specific toppings.
  2. The catch block is where we handle the error if something goes wrong in the try block. e.g. Ordering a pizza with other toppings instead.
  3. The finally block is optional. It contains code that runs whether the try block succeeded or failed. e.g. Paying the bill at the end.

Let’s put this all into perspective with a small example.

const parsedJson = JSON.parse("{name : 'Manmeet'}"); // Uncaught SyntaxError: Expected property name or '}' in JSON at position 1

In the above example, we try to parse invalid JSON using JSON.parse(), and it results in an error Uncaught SyntaxError: Expected property name or '}' in JSON at position 1. Now this could cause issues with our application.

To solve this, we will just gracefully handle the error.

try {
    const parsedJson = JSON.parse("{name : 'Manmeet'}");
} catch (error) {
    console.log("Invalid JSON or unable to parse JSON.");
}
// Output: Invalid JSON or unable to parse JSON.

In the above example, we put the invalid JSON parse code in the try block. If you look at the output, we have the console log from the catch block. So, instead of letting our application crash, we handled the error gracefully and showed a clear message.

Now, let’s check the finally block as well with another example.

function loadUser(json) {
    let loading = true;

    try {
        const user = JSON.parse(json);
        console.log(`Welcome, ${user.name}!`);
    } catch (error) {
        console.log("Couldn't load user data.");
    } finally {
        loading = false;
        console.log("Loading done!");
    }
}

loadUser('{"name": "Manmeet"}');
// Welcome, Manmeet!
// Loading done!

loadUser("{name : 'Manmeet'}");
// Couldn't load user data.
// Loading done!

In the above example, you can see that:

  1. We have a loadUser function which takes a json input and sets a variable loading to true.
  2. The try block tries to parse the json and print a Welcome, <name>! message.
  3. In case of an error, we have a catch block that logs Couldn't load user data.
  4. The finally block sets the loading variable to false and logs Loading done! to the console.
  5. We called the function with valid JSON and got Welcome, Manmeet!. So the try block ran successfully, but we also got Loading done! from the finally block.
  6. We called the function with invalid JSON and got Couldn't load user data. This time, the try block failed and the catch block printed its message, but we also got Loading done! from the finally block.

Pretty neat, right? No matter what happened in try, the finally block always ran. Just like paying the bill at the restaurant, whether you got your pizza or not.

In a real app, this is where you’d do cleanup like hiding a loading spinner, which is exactly what setting loading to false stands for here.

Did you notice something? In all our examples so far, we wrote catch (error) but never actually used error. Let’s see what’s inside it.

try {
    const parsedJson = JSON.parse("{name : 'Manmeet'}");
} catch (error) {
    console.log(error.name);
    console.log(error.message);
}
// SyntaxError
// Expected property name or '}' in JSON at position 1 (line 1 column 2)

In the above example, you can see that:

  1. error.name tells us the type of error. Here, it’s a SyntaxError.
  2. error.message tells us what actually went wrong.

Hmm, you may be wondering: where did this error even come from? We never created it!

When something goes wrong inside the try block, JavaScript creates an object describing the problem and hands it over to the catch block. That’s our error. You can name it anything you like, err or e are pretty common too, but error is the easiest to read.

There’s one more property worth knowing: error.stack.

function loadUser(json) {
    return JSON.parse(json);
}

try {
    loadUser("{name : 'Manmeet'}");
} catch (error) {
    console.log(error.stack);
}
// SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
//     at JSON.parse (<anonymous>)
//     at loadUser (app.js:2:17)
//     at app.js:6:5

In the above example, you can see that:

  1. The first line is the error’s name and message together.
  2. Every line after that tells us where the error happened, and how we got there. JSON.parse failed, which was called inside loadUser on line 2, which was called from line 6.

Think of it as a trail of breadcrumbs that leads you straight to the problem. It’s super handy while debugging, but you’d never show it to your users.

You don’t need to memorise all of them, but you’ll run into these four all the time:

// ReferenceError
console.log(username); // username is not defined

// TypeError
const user = null;
console.log(user.name); // Cannot read properties of null (reading 'name')

// SyntaxError
JSON.parse("{name : 'Manmeet'}"); // Expected property name or '}' in JSON...

// RangeError
const items = new Array(-1); // Invalid array length

In the above example, you can see that:

  1. A ReferenceError happens when you use a variable that doesn’t exist. Sound familiar? We saw this exact error in the closures article, when we tried to log message outside the hello function.
  2. A TypeError happens when you use a value the wrong way, like reading a property from null or calling something that isn’t a function. This is probably the one you’ll see the most.
  3. A SyntaxError happens when JavaScript can’t understand some code or data, like our invalid JSON.
  4. A RangeError happens when a number is outside the allowed range, like an array with a length of -1.

Cool! Now we can catch errors and even read them. But here’s a question: what if something goes wrong that JavaScript doesn’t think is an error?

Here’s the thing: not everything that’s wrong is an error to JavaScript. Some operations run without any complaints, even when the result makes no sense. It’s up to us, as programmers, to tell JavaScript what counts as an error.

Let’s see an example.

const divide = (a, b) => a / b;
console.log(divide(2, 2)); // Outputs 1
console.log(divide(2, 0)); // Outputs Infinity

In the above example, we created a function named divide which takes two inputs and returns the result. Now, for the first call, where we passed (2, 2), the answer is fine. But when we divide by zero, it gives us Infinity.

Wait, what? Infinity? JavaScript didn’t complain at all!

Now imagine using this to split the restaurant bill between your friends, and someone accidentally enters 0 friends. Everyone owes ₹Infinity! 😅 JavaScript is perfectly happy with that answer, but our app definitely shouldn’t be.

Whether it’s a bill-splitting app or a calculator, we don’t want to show Infinity as the result.

So, how do we tell JavaScript that dividing by zero is an error? With the throw keyword.

const divide = (a, b) => {
    if (b === 0) {
        throw new Error("Cannot divide by zero");
    }
    return a / b;
};

try {
    console.log(divide(2, 2));
    console.log(divide(2, 0));
    console.log("This line never runs");
} catch (error) {
    console.log(`${error.name}: ${error.message}`);
}
// 1
// Error: Cannot divide by zero

In the above example, you can see that:

  1. Inside divide, we first check if b is 0.
  2. If it is, we create an error using new Error("Cannot divide by zero") and throw it. This gives us the same kind of error object we saw earlier, with a name, a message and a stack. The only difference is that this time, we wrote the message.
  3. The moment we throw, the function stops right there. The return a / b; line is never reached.
  4. JavaScript then jumps straight to the catch block, skipping everything left in the try block. That’s why "This line never runs" never gets printed.
  5. The first call, divide(2, 2), works just like before and prints 1.

Cool! We just turned a silent Infinity into a proper error that we can catch and handle.

Hmm, you may be wondering: do we always need new Error(...)? Can’t we just throw a string?

Well, you technically can. JavaScript lets you throw anything. But look what happens:

try {
    throw "Cannot divide by zero";
} catch (error) {
    console.log(error); // Cannot divide by zero
    console.log(error.message); // undefined
    console.log(error.stack); // undefined
}

In the above example, you can see that error is just a plain string now. There’s no message, no name and, most importantly, no stack to tell you where things went wrong.

Now you know why one bad line doesn’t have to bring your whole app down.

You learned how to:

  • Use try...catch to handle errors instead of letting your app crash
  • Use finally to run cleanup code whether things succeed or fail
  • Read an error’s name, message and stack
  • Recognise common error types like ReferenceError, TypeError, SyntaxError and RangeError
  • Use throw to raise your own errors when something is wrong but JavaScript doesn’t complain

Things will always go wrong, so plan for it. Just like a good restaurant, a good app doesn’t shut down over one bad order. It handles it and keeps serving.