Intermediate⏱️ 8 min📘 Topic 12 of 13

⚠️ JavaScript Error Handling — try/catch, Custom Errors & Async Errors

Robust error handling in JavaScript — try/catch/finally, throw, custom Error classes, async error patterns and how to fail gracefully. With code examples and interview Q&A.

Things break. Networks fail. Users type weird input. Robust JavaScript expects failure and handles it gracefully.

🛡️ try/catch/finally

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

🚨 Throwing errors

function divide(a, b) {
  if (b === 0) throw new Error('Divide by zero');
  return a / b;
}

🏷️ Custom Error classes

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

🌊 Async errors

A rejected Promise inside await behaves exactly like a thrown error — try/catch catches both.

try {
  const data = await fetch('/api').then(r => r.json());
} catch (err) {
  // Network failure OR JSON parse failure
}

💡 Fail loud OR fail safe — never silently

An empty catch (e) {} is almost always a bug. At minimum, log it.

💻 Code Examples

finally always runs

function test() {
  try {
    return 'a';
  } finally {
    console.log('cleanup');
  }
}
console.log(test());
Output:
cleanup
a

Catching specific error types

try {
  validateUser(data);
} catch (err) {
  if (err instanceof ValidationError) {
    showFieldError(err.field, err.message);
  } else {
    throw err; // rethrow unknown
  }
}
Output:
Handle expected errors, rethrow surprises.

⚠️ Common Mistakes

  • Swallowing errors with empty catch blocks — bugs become invisible.
  • Throwing strings instead of Error objects — you lose the stack trace. Always `throw new Error(...)`.
  • Forgetting that try/catch doesn't catch errors inside setTimeout callbacks or unhandled Promises.
  • Catching too broadly when you only meant to handle one specific case.

🎯 Interview Questions

Real questions asked at top product and service-based companies.

Q1.What does the finally block do?Beginner
It runs whether the try block succeeded or threw. Used for cleanup — closing files, hiding loaders, releasing locks. It runs even if you `return` from try or catch.
Q2.Can try/catch catch errors from async code?Intermediate
It catches synchronous errors AND awaited Promise rejections. It does NOT catch errors inside callbacks (setTimeout, event listeners) or un-awaited Promises — those need their own handling.
Q3.Why throw an Error object instead of a string?Intermediate
Error objects carry a stack trace, a name, and can be extended (custom error classes). Strings can be thrown but lose all that context — making debugging much harder.
Q4.How do you handle an unhandled Promise rejection globally?Advanced
Browsers: `window.addEventListener('unhandledrejection', e => …)`. Node.js: `process.on('unhandledRejection', …)`. Use it to log/telemetry — but fix the root cause, don't rely on it.
Q5.Why might you create a custom Error class?Intermediate
To differentiate error types (`if (e instanceof NetworkError)`), attach extra data (status codes, validation fields) and produce cleaner stack traces. Makes higher-level handlers smarter.

🧠 Quick Summary

  • try/catch/finally handles sync errors and awaited rejections.
  • Always throw Error objects, never strings.
  • Use custom Error subclasses for typed handling.
  • finally runs no matter what — perfect for cleanup.
  • Listen to unhandledrejection / uncaughtException for last-resort logging.