CodingNic

Error Handling

Error Handling Best Practices

Error Handling 15 min read

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 catch block 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/catch is easy to reach for everywhere, “just in case.” But wrapping code in try/catch and 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.

javascript
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:

text
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.

javascript
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:

text
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:

javascript
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:

text
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.

javascript
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 catch block can actually do something useful with.
  • An empty catch block hides real bugs behind code that looks like it’s working; always do something with a caught error, at minimum log it.
  • Check instanceof to handle the specific errors you expect, and rethrow anything else so unexpected bugs stay loud instead of getting buried.
  • Use console.error(), not console.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.