Skip to content

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.

Rajesh DhimanFebruary 08, 2026~20 min
On this page

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.
Companion Book

Looking for structured, offline interview prep?

Free online lessons • Portable Amazon Kindle / Paperback

JavaScript Interview Guide cover
Recommended for Core JS Rounds

JavaScript Interview Guide

By Rajesh Dhiman

Deep dive into the Event Loop, microtask draining, closures, prototypal inheritance, and asynchronous execution patterns.

View on Amazon ↗Kindle & Paperback
Frontend Interview Guide (2025 Edition) cover
Recommended for React & Frontend Rounds

Frontend Interview Guide (2025 Edition)

By Rajesh Dhiman

Comprehensive preparation covering React internals, Web Performance, CSS architecture, and system design questions.

View on Amazon ↗Kindle & Paperback

Note: All lessons on Frontend Interview remain completely free to read online. As an Amazon Associate, we may earn from qualifying purchases.

RD

Written by Rajesh Dhiman

Authored by Rajesh Dhiman and engineered at Eunix Tech. Focused on deep-dive frontend engineering, JavaScript runtime internals, React architecture, and web performance.