JavaScript Error Handling: try/catch, Custom Errors, and Patterns

A deep, practical guide to error handling in JavaScript for frontend interviews: sync vs async errors, try/catch with async/await, custom error classes, cause, and user-friendly error patterns.

F

Frontend Interview Team

February 08, 2026

~20 min
JavaScript Error Handling: try/catch, Custom Errors, and Patterns

Error handling is a “senior signal” topic.

30‑second interview answer

try/catch catches synchronous errors. For async, either await inside try/catch or attach .catch() to the promise. In real apps, create meaningful error types (custom Error classes), keep user-facing messages safe, and log enough context without leaking secrets.

Key points

  • Sync throw → try/catch.
  • Promise rejection → await + try/catch OR .catch.
  • Prefer typed errors with name/message/cause.
  • Separate user message from debug details.

Interviewers want to see if you can:

  • catch errors correctly (sync vs async)
  • create meaningful error types
  • build user-friendly fallback UI
  • log without leaking sensitive data

1) The basics: throw, try/catch

function parseJson(str) {
  try {
    return JSON.parse(str);
  } catch (err) {
    return null;
  }
}

Key idea:

  • try/catch catches synchronous errors thrown inside the try block.

2) Async errors: Promises vs try/catch

A) Promise style

fetch('/api')
  .then((r) => r.json())
  .catch((err) => {
    console.error('Request failed', err);
  });

B) async/await style

async function load() {
  try {
    const res = await fetch('/api');
    if (!res.ok) throw new Error('Bad response');
    return await res.json();
  } catch (err) {
    console.error(err);
    throw err;
  }
}

Interview tip:

  • try/catch works with await because the awaited promise rejection behaves like a thrown error.

3) Common mistake: try/catch doesn’t catch async without await

try {
  fetch('/api').then(() => {
    throw new Error('boom');
  });
} catch (err) {
  // won't run
}

Because the throw happens later, outside the current sync call stack.

Fix:

  • use .catch()
  • or await inside try/catch

4) Create custom errors (this is interview gold)

Custom error classes make debugging and UI decisions easier.

class HttpError extends Error {
  constructor(message, { status, url, cause } = {}) {
    super(message, { cause });
    this.name = 'HttpError';
    this.status = status;
    this.url = url;
  }
}
 
async function fetchJson(url) {
  const res = await fetch(url);
  if (!res.ok) {
    throw new HttpError('Request failed', { status: res.status, url });
  }
  return res.json();
}

Now the caller can do:

try {
  await fetchJson('/api/user');
} catch (err) {
  if (err instanceof HttpError && err.status === 401) {
    // show login UI
  }
}

5) cause (modern JS)

Modern Error supports cause:

try {
  doThing();
} catch (err) {
  throw new Error('Failed to doThing', { cause: err });
}

This helps preserve the original failure while adding context.


6) Pattern: convert unknown errors into a safe shape

In real apps, catch (err) gives unknown in TypeScript. You should normalize it.

function toErrorMessage(err: unknown) {
  if (err instanceof Error) return err.message;
  return String(err);
}

7) Frontend patterns: user-friendly errors

A) Show retryable states

  • network errors → “Retry”
  • empty states → “No results”

B) Log errors, but don’t expose secrets

  • avoid logging tokens, PII
  • redact request headers

C) Errors as part of state

const [state, setState] = useState({ loading: true, data: null, error: null });
 
try {
  const data = await load();
  setState({ loading: false, data, error: null });
} catch (err) {
  setState({ loading: false, data: null, error: toErrorMessage(err) });
}

8) Interview Q&A

Q: What’s the difference between throwing and rejecting?

  • Throwing in async function becomes a rejected promise.

Q: How do you handle errors from fetch?

  • fetch only rejects on network errors. For HTTP errors, check res.ok and throw.

Q: When do you use custom errors?

  • When different failure types need different UI/retry behavior.

Summary checklist

  • I can explain sync vs async error handling.
  • I can explain fetch HTTP errors vs network errors.
  • I know when to create custom errors.
  • I separate user-friendly messages from debug details.

Summary

  • try/catch catches sync errors and awaited async errors.
  • Use .catch for promise chains.
  • Create custom errors for better control and debugging.
  • Normalize unknown errors for safe UI.
  • Always design user-friendly fallback states.