Error Handling Best Practices
Objectives
By the end of this lesson, you should be able to:
- Explain why catching an error you can’t actually handle causes problems
- Recognize why an empty
catchblock is dangerous, and avoid writing one - Decide when to let an error propagate instead of catching it
- Use
console.error()to log errors separately from normal output
💡 Why this matters:
try/catchis easy to reach for everywhere, “just in case.” But wrapping code intry/catchand doing nothing useful with the error doesn’t make your program more reliable, it just hides bugs from you until a user finds them first. Knowing what to catch, and what to leave alone, matters as much as knowing the syntax.
Only Catch What You Can Handle
Catching an error is only useful if your catch block can actually do something sensible with it: retry, use a fallback value, show a helpful message. If you catch an error and don’t know what to do with it, you haven’t solved the problem, you’ve just delayed discovering it.
The Empty catch Block Is Dangerous
Here’s a function with a real bug: it reads user.membership.discount, but sam was never given a membership field.
function getDiscount(user) {
try {
return user.membership.discount;
} catch (error) {
// swallowed silently, nothing logged
}
return 0;
}
const priya = { membership: { discount: 0.2 } };
const sam = { name: "Sam" }; // missing membership, likely a bug upstream
console.log("Priya's discount:", getDiscount(priya));
console.log("Sam's discount:", getDiscount(sam));
Output:
Priya's discount: 0.2
Sam's discount: 0
sam’s missing membership field throws a TypeError inside getDiscount, but the empty catch block swallows it completely. The function just returns 0, quietly, with no error message anywhere. Nothing crashes, which sounds good, but nothing tells you a bug exists either. If sam was supposed to have a membership and doesn’t because of a bug somewhere else in the program, this code will never reveal that. The wrong discount just ships.
An empty catch block is one of the easiest ways to hide a real bug behind a program that looks like it’s working.
Let Unexpected Errors Propagate
Only catch the specific errors you’re prepared for, and let everything else propagate up. Checking with instanceof (from the previous lesson) lets you do exactly that: handle the error you expect, and rethrow anything else.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
function validateOrder(order) {
if (!order.item) {
throw new ValidationError("Order is missing an item");
}
return true;
}
function processOrder(order) {
try {
validateOrder(order);
console.log("Order OK:", order.item);
} catch (error) {
if (error instanceof ValidationError) {
console.error("Invalid order:", error.message);
return;
}
throw error; // not a validation problem, don't pretend to handle it
}
}
processOrder({ item: "keyboard" });
processOrder({});
Output:
Order OK: keyboard
Invalid order: Order is missing an item
processOrder only knows how to handle ValidationError. If validateOrder throws anything else, an unrelated bug for example, processOrder rethrows it with throw error instead of silently treating it like a validation problem. That keeps the catch block honest: it only claims to handle what it actually understands.
To see that rethrow in action, here’s the same code with a real bug added to validateOrder, a .toUpperCase() call on something that isn’t a string:
function validateOrder(order) {
if (!order.item) {
throw new ValidationError("Order is missing an item");
}
order.item.toUpperCase(); // bug: order.item might not be a string
return true;
}
processOrder({ item: 42 });
Output:
TypeError: order.item.toUpperCase is not a function
That crash is a good thing here. The bug in validateOrder isn’t a validation problem, it’s a real defect, and processOrder correctly refuses to catch it and pretend everything is fine. A loud crash during development is far easier to fix than a silently wrong discount shipped to a user.
console.error() for Logging Errors
Use console.error() instead of console.log() when logging an error. It sends output to a separate stream (stderr instead of stdout), so errors can be filtered, redirected, or highlighted separately from normal program output, and most consoles display console.error() output visibly styled (often in red) so it stands out.
console.log("Normal output");
console.error("Something went wrong");
Both lines print in a typical console, but they’re distinguishable: tools that separate stdout from stderr, log viewers, and browser devtools all treat console.error() output differently from console.log(). Reach for it any time you’re logging that something failed, even outside a catch block.
Try It
Take the getDiscount example from earlier in this lesson and fix it: instead of an empty catch block, log the error with console.error(), including the user’s name if you have it, before returning 0. Run it with both priya and sam and confirm the bug for sam is now visible in the output instead of hidden.
Recap
- Only catch errors your
catchblock can actually do something useful with. - An empty
catchblock hides real bugs behind code that looks like it’s working; always do something with a caught error, at minimum log it. - Check
instanceofto handle the specific errors you expect, and rethrow anything else so unexpected bugs stay loud instead of getting buried. - Use
console.error(), notconsole.log(), when logging that something failed.
Next lesson is exercises only: you’ll practice try/catch/finally, throwing errors, and writing custom error classes with no new concepts introduced.