Skip to main content
Robust error handling is a signal of production-ready code. Interviewers look for correct async error handling and meaningful custom error types.

try / catch / finally

try {
  const data = JSON.parse(invalidJson); // throws SyntaxError
} catch (err) {
  console.error(err.name);    // "SyntaxError"
  console.error(err.message); // "Unexpected token..."
  console.error(err.stack);   // Full stack trace
} finally {
  // Runs regardless of whether an error was thrown
  cleanup();
}

Error types

TypeWhen thrown
ErrorBase class for all errors
TypeErrorWrong type used
ReferenceErrorUndefined variable accessed
SyntaxErrorInvalid JS syntax
RangeErrorValue out of valid range
URIErrorInvalid URI

Custom errors

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

class NotFoundError extends Error {
  constructor(resource) {
    super(`${resource} not found`);
    this.name = "NotFoundError";
    this.statusCode = 404;
  }
}

// Usage
try {
  throw new ValidationError("Email is invalid", "email");
} catch (err) {
  if (err instanceof ValidationError) {
    console.log(err.field); // "email"
  }
}

Async error handling

// Promise chains
fetch("/api/data")
  .then(res => {
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  })
  .catch(err => console.error("Fetch failed:", err));

// async/await
async function loadData() {
  try {
    const res = await fetch("/api/data");
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    // handles both network errors and non-ok responses
    throw err; // re-throw after logging
  }
}

// Unhandled promise rejections
process.on("unhandledRejection", (reason) => {
  console.error("Unhandled rejection:", reason);
  process.exit(1);
});

Error propagation patterns

// Re-throw with context
async function getUser(id) {
  try {
    const user = await db.users.findById(id);
    if (!user) throw new NotFoundError("User");
    return user;
  } catch (err) {
    if (err instanceof NotFoundError) throw err; // pass through domain errors
    throw new Error(`Failed to get user ${id}: ${err.message}`); // wrap infra errors
  }
}

// Result pattern (avoid try/catch at call site)
async function safeParseJSON(str) {
  try {
    return { data: JSON.parse(str), error: null };
  } catch (err) {
    return { data: null, error: err };
  }
}

const { data, error } = await safeParseJSON(input);

Common interview questions

The error from finally replaces the original error (or return value). The original error is lost. Avoid throwing in finally — use it only for cleanup.
function risky() {
  try {
    throw new Error("original");
  } finally {
    throw new Error("from finally"); // swallows "original"
  }
}
Promise.all rejects as soon as any promise rejects. The other promises continue running but their results are ignored. Use Promise.allSettled if you need all results regardless of failures, or wrap individual promises in .catch() to prevent Promise.all from short-circuiting.
const results = await Promise.all(
  urls.map(url => fetch(url).catch(err => ({ error: err })))
);
Extending Error gives you: a stack trace, proper instanceof checks, a message property, and correct behavior in error monitoring tools. Always set this.name in the subclass so the error displays correctly.