Error handling

JavaScript · Practical · 12 min

Robust programs fail loudly and recover deliberately instead of crashing or hiding problems.

When something goes wrong, code throws an error, which unwinds the call stack until a try...catch handles it. If nothing does, the error is reported as uncaught (and a Node.js process exits).

try {
  riskyThing();
} catch (err) {
  console.error(err.message);
} finally {
  cleanUp();               // always runs
}

You can throw any value, but always throw Error objects (or subclasses): they carry message, name and a stack trace. Built-in types include TypeError, RangeError, SyntaxError and ReferenceError.

finally runs whether the try block finishes normally, returns or throws — use it to release resources. If you don't need the error, the binding is optional: catch { ... }.

For your own failure cases, subclass Error so callers can tell them apart with instanceof:

class NotFoundError extends Error {
  name = "NotFoundError";
}

When you catch a low-level error and throw a more meaningful one, keep the original with the `cause` option: throw new Error("Could not load config", { cause: err }). Debuggers and Node print the whole chain.

Guidelines that save real pain:

  • Catch only what you can handle; let the rest propagate.
  • Never swallow errors silently with an empty catch.
  • Validate input at boundaries and throw early with a clear message.
  • A try around code that schedules a callback does not catch errors thrown later inside it — asynchronous errors are handled with promises and await, coming up next.

Example

class ValidationError extends Error {
  name = "ValidationError";
  constructor(field, message) {
    super(message);
    this.field = field;
  }
}

function parseAge(input) {
  const age = Number(input);
  if (!Number.isInteger(age) || age < 0) {
    throw new ValidationError("age", `Invalid age: ${input}`);
  }
  return age;
}

function loadProfile(raw) {
  try {
    return { age: parseAge(raw.age) };
  } catch (err) {
    if (err instanceof ValidationError) return { age: null, error: err.message };
    throw new Error("Unexpected profile failure", { cause: err });
  } finally {
    console.log("validated", raw);
  }
}

console.log(loadProfile({ age: "42" }));   // { age: 42 }
console.log(loadProfile({ age: "-1" }));   // { age: null, error: 'Invalid age: -1' }

Remember

  • Throw Error objects (or subclasses), never strings
  • finally always runs — use it for cleanup
  • Wrap low-level errors with { cause } instead of discarding them

Watch out for

  • An empty catch {} that hides bugs.
  • Expecting a synchronous try/catch to catch errors thrown later in a callback.

Check yourself

  1. function f() { try { return "try"; } finally { console.log("cleanup"); } } — what does calling f() do?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch