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.
Analogy
Section titled “Analogy”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.
What is Error Handling?
Section titled “What is Error Handling?”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:
- The
tryblock contains the code that does what you originally wanted. e.g. Ordering a pizza with specific toppings. - The
catchblock is where we handle the error if something goes wrong in thetryblock. e.g. Ordering a pizza with other toppings instead. - The
finallyblock is optional. It contains code that runs whether thetryblock 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:
- We have a
loadUserfunction which takes ajsoninput and sets a variableloadingtotrue. - The
tryblock tries to parse thejsonand print aWelcome, <name>!message. - In case of an error, we have a
catchblock that logsCouldn't load user data. - The
finallyblock sets theloadingvariable tofalseand logsLoading done!to the console. - We called the function with valid JSON and got
Welcome, Manmeet!. So thetryblock ran successfully, but we also gotLoading done!from thefinallyblock. - We called the function with invalid JSON and got
Couldn't load user data.This time, thetryblock failed and thecatchblock printed its message, but we also gotLoading done!from thefinallyblock.
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.
The Error Object
Section titled “The Error Object”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:
error.nametells us the type of error. Here, it’s aSyntaxError.error.messagetells 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:
- The first line is the error’s
nameandmessagetogether. - Every line after that tells us where the error happened, and how we got there.
JSON.parsefailed, which was called insideloadUseron 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.
Common Types of Errors
Section titled “Common Types of Errors”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:
- A
ReferenceErrorhappens when you use a variable that doesn’t exist. Sound familiar? We saw this exact error in the closures article, when we tried to logmessageoutside thehellofunction. - A
TypeErrorhappens when you use a value the wrong way, like reading a property fromnullor calling something that isn’t a function. This is probably the one you’ll see the most. - A
SyntaxErrorhappens when JavaScript can’t understand some code or data, like our invalid JSON. - A
RangeErrorhappens 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?
Throwing Errors
Section titled “Throwing Errors”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:
- Inside
divide, we first check ifbis0. - If it is, we create an error using
new Error("Cannot divide by zero")andthrowit. This gives us the same kind of error object we saw earlier, with aname, amessageand astack. The only difference is that this time, we wrote the message. - The moment we
throw, the function stops right there. Thereturn a / b;line is never reached. - JavaScript then jumps straight to the
catchblock, skipping everything left in thetryblock. That’s why"This line never runs"never gets printed. - The first call,
divide(2, 2), works just like before and prints1.
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.
🧵 Wrapping It Up
Section titled “🧵 Wrapping It Up”Now you know why one bad line doesn’t have to bring your whole app down.
You learned how to:
- Use
try...catchto handle errors instead of letting your app crash - Use
finallyto run cleanup code whether things succeed or fail - Read an error’s
name,messageandstack - Recognise common error types like
ReferenceError,TypeError,SyntaxErrorandRangeError - Use
throwto 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.