Error Handling

Things go wrong: networks fail, users type unexpected input, data arrives in the wrong shape. Good error handling means anticipating failures, reporting them clearly, and keeping the rest of the program working. JavaScript's tools are throw, try/catch/finally, and the Error family of objects.

What happens without handling

JS uncaught.js
const settings = JSON.parse('{"theme": "dark"}');
console.log("theme:", settings.theme);

const broken = JSON.parse("{theme: dark}");
console.log("this line never runs");
Output
theme: dark
Uncaught SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)

An uncaught error stops the current script at that line. In a browser, other event handlers keep working, but whatever that code was meant to do is left half-done.

try, catch and finally

JS try-catch.js
function parseSettings(text) {
  try {
    const parsed = JSON.parse(text);
    return parsed;
  } catch (error) {
    console.log(`Bad settings (${error.name}), using defaults`);
    return { theme: "light" };
  } finally {
    console.log("parse attempt finished");
  }
}

console.log(parseSettings('{"theme":"dark"}'));
console.log(parseSettings("not json"));
Output
parse attempt finished
{ theme: 'dark' }
Bad settings (SyntaxError), using defaults
parse attempt finished
{ theme: 'light' }
  • try wraps code that might fail.
  • catch runs only if something in try threw. It receives the error. If you don't need it, write just catch { … }.
  • finally always runs — after success, after an error, even after a return. Use it for cleanup like closing connections or re-enabling buttons.

Error objects

Every error has a name, a message and usually a stack trace showing where it happened. JavaScript's built-in types tell you what went wrong:

TypeTypical cause
TypeErrorUsing a value the wrong way: calling a non-function, reading a property of undefined
ReferenceErrorUsing a variable that doesn't exist
SyntaxErrorInvalid code, or invalid JSON passed to JSON.parse
RangeErrorA number outside its allowed range: new Array(-1), toFixed(200)
JS types.js
const tests = [
  () => null.length,
  () => missingVariable,
  () => (5).toFixed(500),
];

for (const test of tests) {
  try {
    test();
  } catch (err) {
    console.log(`${err.name}: ${err instanceof TypeError ? "type problem" : "other"}`);
  }
}
Output
TypeError: type problem
ReferenceError: other
RangeError: other

Throwing your own errors

Use throw when your code detects a problem it can't sensibly handle itself. Always throw Error objects (not strings), so you get a stack trace:

JS throw.js
function withdraw(balance, amount) {
  if (typeof amount !== "number" || Number.isNaN(amount)) {
    throw new TypeError("Amount must be a number");
  }
  if (amount > balance) {
    throw new RangeError(`Cannot withdraw ${amount}; balance is ${balance}`);
  }
  return balance - amount;
}

for (const amount of [30, 500, "ten"]) {
  try {
    console.log("new balance:", withdraw(100, amount));
  } catch (err) {
    console.log(`${err.name}: ${err.message}`);
  }
}
Output
new balance: 70
RangeError: Cannot withdraw 500; balance is 100
TypeError: Amount must be a number

Custom error classes

Subclassing Error lets callers tell different failures apart and carry extra information:

JS custom.js
class HttpError extends Error {
  constructor(status, message) {
    super(message);
    this.name = "HttpError";
    this.status = status;
  }
}

function handle(err) {
  if (err instanceof HttpError && err.status === 404) return "Show 'not found' page";
  if (err instanceof HttpError && err.status >= 500) return "Show 'try again later'";
  throw err;
}

console.log(handle(new HttpError(404, "No such page")));
console.log(handle(new HttpError(503, "Down for maintenance")));
Output
Show 'not found' page
Show 'try again later'

Notice throw err at the end: handle only the errors you understand and rethrow the rest. Silently swallowing unknown errors hides bugs.

Wrapping errors with cause

When you catch a low-level error and throw a more meaningful one, keep the original as its cause:

JS cause.js
function loadConfig(text) {
  try {
    return JSON.parse(text);
  } catch (err) {
    throw new Error("Config file is corrupted", { cause: err });
  }
}

try {
  loadConfig("{oops");
} catch (err) {
  console.log(err.message, "← caused by", err.cause.name);
}
Output
Config file is corrupted ← caused by SyntaxError

Async errors

try/catch only catches errors thrown synchronously inside it, or by an awaited promise. A callback that runs later is outside it:

JS async-errors.js
try {
  setTimeout(() => {
    try {
      throw new Error("inside timer");
    } catch (err) {
      console.log("caught inside the callback:", err.message);
    }
  }, 0);
} catch {
  console.log("never reached — the timer ran later");
}

async function main() {
  try {
    await Promise.reject(new Error("rejected promise"));
  } catch (err) {
    console.log("await + try/catch works:", err.message);
  }
}
main();
Output
await + try/catch works: rejected promise
caught inside the callback: inside timer

Global safety nets

As a last resort — for logging, not normal handling — browsers fire events for errors nobody caught:

JS global.js
window.addEventListener("error", event => {
  reportToServer({ message: event.message, file: event.filename, line: event.lineno });
});

window.addEventListener("unhandledrejection", event => {
  reportToServer({ message: String(event.reason) });
});

Error-monitoring services such as Sentry hook into these events to collect crashes from real users.

Guidelines

  • Catch errors where you can do something useful: retry, show a message, use a fallback.
  • Show users a friendly message; log the technical details for developers.
  • Never leave a catch block empty without a comment explaining why.
  • Validate input early and throw clear errors, rather than letting bad data cause confusing failures later.