Topics in this subject
JavaScript 2 min read Updated 4 Aug 2026

Part 15 — Error Handling

Error types, Custom errors 🟢, Async errors ⚠️ 🎯

try {
  risky();
} catch (err) {           // ES2019+: catch binding is optional: `catch {`
  console.error(err.message);
} finally {
  cleanup();              // always runs (even on return/throw)
}

throw new Error("boom");  // throw anything, but prefer Error objects

Error types#

Error, TypeError, RangeError, ReferenceError, SyntaxError, URIError, EvalError, and AggregateError (from Promise.any). Each has .name, .message, .stack, and (modern) .cause.

Custom errors 🟢#

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
  }
}
try {
  throw new ValidationError("email required", "email");
} catch (e) {
  if (e instanceof ValidationError) console.log(e.field);
}

// error chaining (ES2022)
throw new Error("failed to load config", { cause: originalError });

Async errors ⚠️ 🎯#

// try/catch does NOT catch errors from a rejected promise you didn't await
try { doAsync(); } catch {}          // ❌ won't catch async rejection
try { await doAsync(); } catch {}    // ✅ awaited → caught

// .catch for promise chains
fetch(url).then(r => r.json()).catch(err => /* handle */);

// finally on promises
p.finally(() => spinner.hide());

Unhandled rejections: browser fires window.onunhandledrejection; Node emits unhandledRejection (and by default crashes on it in recent versions).

Best practices: throw Error subclasses, never swallow errors silently, add context via cause, fail fast, handle at the boundary (route/UI layer), and don't use exceptions for normal control flow.

Interview questions (Part 15):

  1. Does finally run if try returns? If it throws?
  2. Why doesn't try/catch catch an un-awaited async error?
  3. How do you create and detect a custom error type?