Error Handling Patterns in Large JavaScript Apps

    Master professional error handling strategies for enterprise-scale applications

    What You'll Learn

    • Layered try/catch boundaries
    • Centralized error handlers
    • Defensive programming techniques
    • Global Promise rejection handling
    • Custom error classes
    • Retry logic with exponential backoff

    💡 Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:

    • Download Node.js to run JavaScript on your computer
    • Use your browser's Developer Console (Press F12) to test code snippets
    • Create a .html file with <script> tags and open it in your browser

    Error handling in small scripts is simple: wrap things in try/catch and log a message. But once you start building real applications — dashboards, ecommerce sites, admin tools, mobile apps, AI tools — everything becomes more complex.

    Why Error Handling Changes in Big Apps

    Large applications have:

    • many API calls happening at once
    • shared state across components
    • multiple async operations
    • user-driven events
    • dynamic rendering
    • background tasks
    • cross-origin requests
    • server-side + client-side logic

    Pattern 1 — Layered try/catch

    Instead of wrapping EVERYTHING in try/catch, you wrap logical boundaries.

    Layered try/catch

    Wrap logical boundaries instead of wrapping everything

    Try it Yourself »
    JavaScript
    async function initializeApp() {
      try {
        await loadUser();
        await loadDashboard();
        await connectToSocket();
      } catch (err) {
        handleGlobalError(err);
      }
    }

    Pattern 2 — Centralized Error Handler

    Instead of writing console.error everywhere, use a shared handler.

    Centralized Error Handler

    Use a shared handler instead of console.error everywhere

    Try it Yourself »
    JavaScript
    function handleError(error, context = "") {
      console.group(`Error in ${context}`);
      console.error(error);
      console.groupEnd();
    
      // Optional: send to server
      // sendToErrorTracker({ error, context });
    }
    
    // Usage:
    try {
      riskyOp();
    } catch (err) {
      handleError(err, "riskyOp");
    }

    Pattern 3 — Defensive Programming

    Instead of assuming everything exists, verify it.

    Defensive Programming

    Verify everything exists instead of assuming

    Try it Yourself »
    JavaScript
    const el = document.querySelector("#profile");
    if (!el) return; // fail early
    
    // Check API responses
    if (!data || !data.user) throw new Error("Malformed response");
    
    // Check input types
    if (typeof callback !== "function") return;

    Pattern 4 — Guarding Async Code

    In big apps, async errors often get lost.

    ❌ Wrong

    Wrong Async Error Handling

    Silent failure when not using try/catch

    Try it Yourself »
    JavaScript
    async function load() {
      const data = await fetchData(); // if this rejects → silent failure
    }

    ✔ Right

    Right Async Error Handling

    Proper try/catch for async functions

    Try it Yourself »
    JavaScript
    async function load() {
      try {
        const data = await fetchData();
      } catch (err) {
        handleError(err, "load");
      }
    }

    Even better — wrap async functions in a helper:

    Safe Async Helper

    Wrap async functions in a helper for Go-style error handling

    Try it Yourself »
    JavaScript
    async function safe(fn) {
      try {
        return [await fn(), null];
      } catch (err) {
        return [null, err];
      }
    }
    
    // Usage:
    const [data, error] = await safe(() => fetchData());
    // No crashes, no lost errors.

    Pattern 5 — Global Promise Rejection Handler

    Large apps must handle unhandled Promise rejections:

    Global Promise Rejection Handler

    Catch all unhandled Promise rejections

    Try it Yourself »
    JavaScript
    window.addEventListener("unhandledrejection", (event) => {
      handleError(event.reason, "Unhandled Promise");
    });

    Pattern 6 — Error Boundaries (React)

    Error Boundary (React)

    Allow components to fail without breaking entire UI

    Try it Yourself »
    JavaScript
    class ErrorBoundary extends React.Component {
      state = { hasError: false };
    
      componentDidCatch(error, info) {
        this.setState({ hasError: true });
      }
    
      render() {
        return this.state.hasError
          ? <FallbackUI />
          : this.props.children;
      }
    }

    Pattern 7 — Custom Error Classes

    Custom Error Classes

    Create specific error types for better handling

    Try it Yourself »
    JavaScript
    class ValidationError extends Error {
      constructor(message) {
        super(message);
        this.name = "ValidationError";
      }
    }
    
    class ApiError extends Error {
      constructor(message, status) {
        super(message);
        this.name = "ApiError";
        this.status = status;
      }
    }
    
    // Usage:
    try {
      throw new ApiError("Not Found", 404);
    } catch (err) {
      if (err instanceof ApiError) {
        console.error("API error:", err.status);
      }
    }

    Pattern 8 — Retry Logic with Backoff

    Retry Logic with Backoff

    Exponential backoff doubles wait time between retries

    Try it Yourself »
    JavaScript
    async function retry(fn, retries = 3, delay = 500) {
      try {
        return await fn();
      } catch (err) {
        if (retries === 0) throw err;
    
        await new Promise(res => setTimeout(res, delay));
        return retry(fn, retries - 1, delay * 2); // exponential backoff
      }
    }
    
    // Usage:
    const data = await retry(() => fetch("/api/data").then(r => r.json()));
    
    // Dramatically reduces user errors for poor mobile or public WiFi networks.

    Pattern 9 — AbortController for Timeouts

    AbortController for Timeouts

    Ensure the user always gets feedback with timeouts

    Try it Yourself »
    JavaScript
    function fetchWithTimeout(url, ms = 5000) {
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), ms);
    
      return fetch(url, { signal: controller.signal })
        .finally(() => clearTimeout(timer));
    }
    
    // Handle timeout:
    try {
      const res = await fetchWithTimeout("/api/data", 3000);
    } catch (error) {
      if (error.name === "AbortError") {
        console.error("Request timed out");
      }
    }

    Pattern 10 — Circuit Breaker

    If an API repeatedly fails, the app temporarily stops calling it:

    Circuit Breaker Pattern

    Stop calling failing APIs temporarily (Netflix-inspired)

    Try it Yourself »
    JavaScript
    class CircuitBreaker {
      constructor(failureLimit = 3, resetTime = 5000) {
        this.failures = 0;
        this.state = "CLOSED";
        this.failureLimit = failureLimit;
        this.resetTime = resetTime;
      }
    
      async call(fn) {
        if (this.state === "OPEN") {
          throw new Error("Circuit is open");
        }
    
        try {
          const result = await fn();
          this.failures = 0;
          return result;
        } catch (err) {
          this.failures++;
          if (this.failures >= this.failureLimit) {
            this.state 
    ...

    What You Learned

    • Why error handling is different in large apps
    • Layered try/catch boundaries
    • Centralized error handlers
    • Defensive programming techniques
    • Async error guarding
    • Global error listeners
    • Error boundaries in React
    • Custom error classes
    • Retry logic with exponential backoff
    • Timeout handling with AbortController
    • Circuit breaker pattern

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous