Tutorial JavaScript Developer Tools Cheat Sheet

Free JavaScript Error Handling Patterns Cheat Sheet Online — Interactive Reference for Developers

· 18 min read

Errors are inevitable in software. Networks drop packets, users submit invalid data, APIs return unexpected formats, and developers make mistakes. JavaScript, running in an event-driven single-threaded environment, presents unique challenges for error handling. An unhandled exception in a synchronous callback can crash a Node.js server. An unhandled Promise rejection in a browser can silently fail, leaving users staring at a frozen spinner. Even experienced developers search daily for javascript error handling patterns, try catch javascript syntax, and async await error handling best practices.

The problem is not a lack of documentation. MDN exhaustively documents every error type and every API. Stack Overflow contains millions of answers about javascript promise catch behavior and fetch error handling. The problem is retrieval friction — finding the right pattern at the right moment, understanding the subtle differences between Promise.all and Promise.allSettled, remembering that fetch does not throw on HTTP 404, or knowing when to use window.onerror versus unhandledrejection.

That is why we built the free interactive JavaScript Error Handling Patterns Cheat Sheet. It organizes 45+ error handling patterns into a searchable, filterable reference with copy-ready code examples. Whether you are debugging a production incident, writing defensive code for a new feature, or preparing for a technical interview, every pattern is at your fingertips. Everything runs client-side in your browser — no data ever leaves your machine.

Skip ahead: If you want to explore the tool while reading, open the JavaScript Error Handling Patterns Cheat Sheet in another tab. Use it to test the code examples in this article in real time.

Why JavaScript Error Handling Is Uniquely Challenging

JavaScript runs in two fundamentally different environments — browsers and Node.js — with different global error handling mechanisms. It supports both synchronous and asynchronous execution models, each with distinct error propagation paths. It has built-in error types that behave differently depending on context. And its most common networking API, fetch(), has counterintuitive error behavior that trips up even senior developers.

In a browser, an uncaught synchronous error triggers window.onerror. An unhandled Promise rejection triggers unhandledrejection. A resource loading error (broken image, failed script) triggers error on the element and bubbles to window. In Node.js, uncaught exceptions trigger process.on('uncaughtException') while unhandled rejections trigger process.on('unhandledRejection'). These handlers have different signatures, different best practices, and different consequences.

Asynchronous error handling adds another dimension. A Promise rejection without a .catch() handler becomes an unhandled rejection — but only after the microtask queue clears. An async function with an unawaited Promise inside a try/catch block silently bypasses the catch. Promise.all() rejects immediately on the first failure, abandoning all other pending promises. Promise.allSettled() waits for everything but returns a more complex data structure. Understanding these nuances is essential for writing robust JavaScript.

Try / Catch / Finally: The Foundation

The try...catch statement is the most fundamental error handling construct in JavaScript. It executes the catch block when any error is thrown in the try block. This works for synchronous code and, when combined with await, for asynchronous code too.

Basic Try / Catch

try {
  const result = riskyOperation();
  console.log(result);
} catch (error) {
  console.error('Operation failed:', error.message);
}

The catch binding receives an Error object (or any value that was thrown). By convention, this parameter is named error, err, or e. The error object contains message, name, and stack properties.

Try / Catch / Finally

The finally block always executes regardless of whether an error occurred. This makes it ideal for cleanup operations — closing files, releasing locks, clearing timers, or resetting UI state.

const file = openFile('data.txt');
try {
  const data = file.read();
  process(data);
} catch (error) {
  console.error('Read failed:', error);
} finally {
  file.close(); // Always runs, even if read() succeeds
}

Importantly, if the finally block throws an error, that error replaces any previous error. If finally contains a return statement, it overrides any return or throw from the try/catch blocks.

Conditional Catch with instanceof

Not all errors should be handled the same way. Use instanceof to route different error types to different handlers.

try {
  parseUserInput(input);
} catch (error) {
  if (error instanceof TypeError) {
    showToast('Invalid input type');
  } else if (error instanceof RangeError) {
    showToast('Value out of range');
  } else {
    logToSentry(error);
    showToast('Unexpected error');
  }
}

This pattern requires custom error classes or the built-in error hierarchy to work effectively. We will cover custom errors later in this article.

Optional Catch Binding (ES2019)

Starting with ES2019, the catch binding is optional. Use this when you only care that an operation failed, not why.

try {
  JSON.parse(maybeJson);
} catch {
  // We only care that it failed, not why
  useDefaultConfig();
}

This produces slightly cleaner code and avoids unused variable warnings from linters.

Error Wrapping with Cause (ES2022)

ES2022 introduced the cause option for the Error constructor. This lets you wrap low-level errors with high-level context while preserving the original error for debugging.

try {
  fetchUserData(userId);
} catch (error) {
  throw new Error(
    `Failed to load user ${userId}`,
    { cause: error }
  );
}

// Later:
catch (error) {
  console.log(error.message); // "Failed to load user 123"
  console.log(error.cause);   // original fetch error
}

This is one of the most important patterns for building layered architectures. Each layer adds context without losing the root cause.

Promise Error Handling

Promises introduced a fundamentally different error model to JavaScript. Instead of throwing errors up the call stack, they carry rejections through a chain of .then() and .catch() handlers.

Promise.prototype.catch()

The .catch() method attaches a rejection handler to a promise. It returns a new promise, enabling chaining.

fetch('/api/user')
  .then(res => res.json())
  .catch(error => {
    console.error('Fetch failed:', error);
    return { error: true };
  });

A critical detail: .catch() only handles rejections that occur before it in the chain. If the handler passed to .catch() itself throws, the new promise rejects with that error.

Promise.all vs Promise.allSettled

Promise.all() rejects immediately when any promise rejects. The other pending promises are abandoned — their results are lost.

Promise.all([
  fetch('/api/a'),
  fetch('/api/b'),
  fetch('/api/c'),
])
  .then(results => console.log(results))
  .catch(error => {
    // Only the FIRST rejection is caught here
    console.error('At least one failed:', error);
  });

Promise.allSettled(), introduced in ES2020, waits for all promises regardless of outcome. It returns an array of objects describing each result.

const results = await Promise.allSettled([
  fetch('/api/a'),
  fetch('/api/b'),
]);

results.forEach(result => {
  if (result.status === 'fulfilled') {
    console.log('Success:', result.value);
  } else {
    console.error('Failed:', result.reason);
  }
});

Use Promise.all() when any failure means the entire operation should abort. Use Promise.allSettled() when you need to know the outcome of every promise and can handle partial failures.

Promise.race with Timeout

Promise.race() resolves or rejects as soon as any promise settles. This is the standard pattern for adding timeouts to async operations.

function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), ms)
  );
  return Promise.race([promise, timeout]);
}

withTimeout(fetch('/api/slow'), 5000)
  .then(data => console.log(data))
  .catch(err => console.error(err.message)); // "Timeout"

Promise.any()

Promise.any() returns the first fulfilled promise. It only rejects if ALL promises reject, in which case it throws an AggregateError containing all rejection reasons.

try {
  const fastest = await Promise.any([
    fetchFromCDN1(),
    fetchFromCDN2(),
    fetchFromCDN3(),
  ]);
  return fastest;
} catch (aggregateError) {
  console.error('All CDNs failed:', aggregateError.errors);
}

Async / Await Error Handling

Async/await makes asynchronous code look synchronous, but error handling requires attention to detail. The most common mistake is returning a promise without awaiting it inside a try/catch block.

The Classic Await Mistake

// BAD: try/catch does NOT catch this
async function bad() {
  try {
    return fetch('/api'); // Returns promise, does not await
  } catch (error) {
    // Never reached for fetch failures
  }
}

// GOOD: await inside try
async function good() {
  try {
    return await fetch('/api'); // Awaits, so catch works
  } catch (error) {
    console.error('Caught:', error);
  }
}

This distinction is subtle but critical. When you return a promise without await, the try block succeeds immediately (it returns a promise object). The promise may later reject, but by then execution has left the try block.

Sequential Async with Error Isolation

When loading multiple independent resources, wrap each await in its own try/catch so one failure does not abort the entire sequence.

async function loadDashboard() {
  let user = null;
  let posts = [];
  let notifications = [];

  try {
    user = await fetchUser();
  } catch (e) {
    console.error('User load failed');
  }

  try {
    posts = await fetchPosts();
  } catch (e) {
    console.error('Posts load failed');
  }

  try {
    notifications = await fetchNotifications();
  } catch (e) {
    console.error('Notifications load failed');
  }

  return { user, posts, notifications };
}

Parallel Async with Individual Error Handling

For parallel execution, attach catch handlers to individual promises before passing them to Promise.all.

async function loadInParallel() {
  const userPromise = fetchUser().catch(() => null);
  const postsPromise = fetchPosts().catch(() => []);
  const statsPromise = fetchStats().catch(() => ({}));

  const [user, posts, stats] = await Promise.all([
    userPromise, postsPromise, statsPromise
  ]);

  return { user, posts, stats };
}

This pattern runs all fetches concurrently but provides fallback values for any that fail. The dashboard renders even if some data is missing.

Custom Error Classes

Built-in error types cover generic cases, but real applications need domain-specific errors. Custom error classes enable precise error handling, better error messages, and structured logging.

Extending Error

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

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

Always set this.name to the class name. This ensures the stack trace and toString() output identify the correct error type. When extending Error in transpiled code, you may also need Object.setPrototypeOf(this, new.target.prototype) for instanceof to work correctly in some environments.

HTTP-Specific Errors

class HttpError extends Error {
  constructor(response) {
    super(`HTTP ${response.status}: ${response.statusText}`);
    this.name = 'HttpError';
    this.status = response.status;
  }
}

class ClientError extends HttpError {}
class ServerError extends HttpError {}

async function apiFetch(url) {
  const res = await fetch(url);
  if (!res.ok) {
    const ErrorClass = res.status >= 500 ? ServerError : ClientError;
    throw new ErrorClass(res);
  }
  return res.json();
}

Error Codes for Programmatic Handling

Add machine-readable error codes alongside human messages. This enables conditional logic without fragile string matching.

class AppError extends Error {
  constructor(code, message, details = {}) {
    super(message);
    this.name = 'AppError';
    this.code = code;
    this.details = details;
  }
}

// Usage:
throw new AppError(
  'USER_NOT_FOUND',
  'The requested user does not exist',
  { userId: 123 }
);

// Handler:
catch (error) {
  if (error.code === 'USER_NOT_FOUND') {
    redirectToOnboarding();
  }
}

Global Error Handlers

Global handlers are the safety net of last resort. They catch errors that bypass all local handlers. Every production application should have them.

window.onerror (Browser)

Captures uncaught synchronous exceptions in the main thread. Return true to prevent the default browser behavior (console error message).

window.onerror = (message, source, lineno, colno, error) => {
  logToService({
    type: 'uncaught_exception',
    message,
    source,
    lineno,
    colno,
    stack: error?.stack,
  });
  return true; // Prevent default browser handling
};

unhandledrejection (Browser)

Fires when a Promise rejects without a .catch() handler. This is one of the most important events for async error monitoring.

window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled promise rejection:', event.reason);
  logToService({
    type: 'unhandled_rejection',
    reason: event.reason?.message || event.reason,
    stack: event.reason?.stack,
  });
  event.preventDefault();
});

Node.js process.on("uncaughtException")

In Node.js, uncaughtException is the last resort for synchronous errors. After this event fires, the application is in an undefined state. You must exit.

process.on('uncaughtException', (error, origin) => {
  console.error(`Uncaught Exception at ${origin}`);
  console.error(error);

  // Graceful shutdown
  server.close(() => {
    process.exit(1);
  });

  // Force exit if graceful shutdown hangs
  setTimeout(() => process.exit(1), 5000);
});

Fetch and Network Error Handling

The fetch API has counterintuitive error behavior that confuses many developers. It does not throw on HTTP error status codes.

Always Check response.ok

const response = await fetch('/api/data');

if (!response.ok) {
  // response.ok is false for 4xx and 5xx
  throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();

fetch() only rejects on network-level failures: DNS errors, CORS violations, connection resets, and request timeouts (if configured via AbortController). A 404 or 500 response resolves successfully.

AbortController for Timeouts

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);

try {
  const response = await fetch('/api/slow', {
    signal: controller.signal,
  });
  clearTimeout(timeoutId);
  return response.json();
} catch (error) {
  if (error.name === 'AbortError') {
    console.error('Request timed out');
  } else {
    console.error('Network error:', error);
  }
}

Retry with Exponential Backoff

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  for (let i = 0; i <= maxRetries; i++) {
    try {
      const res = await fetch(url, options);
      if (res.ok) return res;
      if (res.status < 500) throw new Error(`Client error ${res.status}`);
    } catch (error) {
      if (i === maxRetries) throw error;
    }
    const delay = Math.pow(2, i) * 1000;
    await new Promise(r => setTimeout(r, delay));
  }
}

Built-in JavaScript Error Types

JavaScript provides several built-in error constructors. Understanding them helps with debugging and enables precise error handling.

TypeError

Thrown when a value is not of the expected type, or when accessing properties of null or undefined. This is the most common runtime error in JavaScript applications.

null.toString();           // TypeError: Cannot read properties of null
undefined.foo;              // TypeError: Cannot read properties of undefined
(42).toUpperCase();         // TypeError: toUpperCase is not a function

ReferenceError

Thrown when referencing a variable that does not exist in the current scope. Also thrown for temporal dead zone violations with let and const.

console.log(undefinedVar);  // ReferenceError: undefinedVar is not defined
let x = y;                   // ReferenceError: Cannot access 'y' before initialization

RangeError

Thrown when a numeric value is outside the allowed range, or when the call stack exceeds its maximum size.

new Array(-1);             // RangeError: Invalid array length
(1).toFixed(101);            // RangeError: toFixed() digits argument out of range
function recurse() { recurse(); }
recurse();                   // RangeError: Maximum call stack size exceeded

SyntaxError

Thrown when the JavaScript engine encounters invalid syntax. Note that SyntaxErrors in loaded scripts cannot be caught at runtime in the same file. However, SyntaxErrors from eval(), Function(), or JSON.parse() can be caught.

JSON.parse('{ invalid }'); // SyntaxError: Unexpected token

try {
  JSON.parse('{ invalid }');
} catch (e) {
  if (e instanceof SyntaxError) {
    console.log('Invalid JSON:', e.message);
  }
}

AggregateError

Represents multiple errors in a single exception. Used by Promise.any() when all promises reject, and available for custom use.

try {
  await Promise.any([reject1, reject2, reject3]);
} catch (error) {
  console.log(error instanceof AggregateError); // true
  console.log(error.errors.length);             // 3
  error.errors.forEach(e => console.log(e.message));
}

Error Handling Best Practices

Beyond syntax and API knowledge, effective error handling requires discipline and architecture. Here are the patterns that separate robust applications from fragile ones.

Fail Fast with Explicit Throws

Validate inputs at function boundaries and throw immediately. Do not let invalid data propagate through your system.

function calculateDiscount(price, discountPercent) {
  if (typeof price !== 'number' || price < 0) {
    throw new TypeError('price must be a non-negative number');
  }
  if (typeof discountPercent !== 'number' ||
      discountPercent < 0 || discountPercent > 100) {
    throw new RangeError('discountPercent must be 0-100');
  }
  return price * (discountPercent / 100);
}

Never Swallow Errors Silently

Empty catch blocks are one of the most destructive patterns in JavaScript. They hide bugs, obscure failures, and make debugging impossible.

// BAD: Error disappears
} catch (e) { }

// BETTER: Log and re-throw
} catch (error) {
  logger.error('Database query failed', error);
  throw error;
}

// BEST: Handle meaningfully
} catch (error) {
  if (isRetryable(error)) {
    return retry(operation);
  }
  logger.error('Fatal:', error);
  throw error;
}

Distinguish Operational vs Programmer Errors

Operational errors are expected runtime failures: network timeouts, disk full, invalid user input. Programmer errors are bugs: null references, type mismatches, invariant violations. Operational errors should be handled gracefully. Programmer errors should crash fast to expose the bug.

Centralized Error Logging

Route all errors through a single logger to ensure consistent formatting, sampling, and transport to monitoring services.

class ErrorLogger {
  static log(error, context = {}) {
    const payload = {
      message: error.message,
      stack: error.stack,
      name: error.name,
      timestamp: new Date().toISOString(),
      ...context,
    };

    if (process.env.NODE_ENV === 'production') {
      sendToSentry(payload);
    } else {
      console.error('[ERROR]', payload);
    }
  }
}

React Error Boundaries

In React applications, Error Boundaries catch rendering errors in child components without crashing the entire application tree.

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    logErrorToService(error, info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return <FallbackUI />;
    }
    return this.props.children;
  }
}

Defensive Default Values

When recovering from errors, provide sensible defaults instead of null or undefined to prevent cascading failures.

let config;
try {
  config = JSON.parse(localStorage.getItem('config'));
} catch {
  config = getDefaultConfig();
}

// Ensure config is always a valid object
config = config || getDefaultConfig();

function getDefaultConfig() {
  return { theme: 'light', language: 'en', notifications: true };
}

Related Tools and References

Error handling is just one part of writing robust JavaScript. Explore these related tools in the DevToolkit collection:

Try the Interactive Cheat Sheet

Reading about error handling is valuable, but having a searchable reference at your fingertips is transformative. Our free interactive JavaScript Error Handling Patterns Cheat Sheet includes all 45+ patterns from this article, organized into filterable categories, with syntax highlighting and one-click copy. Whether you need a quick reminder of Promise.allSettled behavior, a custom error class template, or the exact signature of window.onerror, it is all there.

The cheat sheet runs entirely in your browser. No account required. No data collection. Just open it and start searching.

Found this useful? Check out our free developer tools or browse more articles.