The React Suspense boundary, while excellent for managing loading states, doesn’t inherently catch errors thrown by its children.

Here’s why this happens and how to fix it:

The Problem: Suspense Boundary vs. Error Boundary

Suspense is designed to handle asynchronous operations that result in a "not ready yet" state. When a component inside a Suspense boundary throws a promise that rejects, or a synchronous error, Suspense itself doesn’t have a built-in mechanism to catch that error and render a fallback UI. It expects the error to propagate up the tree until it hits an Error Boundary.

Common Causes and Fixes

  1. No Error Boundary Above Suspense: This is the most common reason. Suspense and Error Boundaries are complementary. Suspense handles loading, Error Boundaries handle errors. If there’s no Error Boundary wrapping the component that throws the error (even if it’s also wrapped by Suspense), the error will crash your application.

    • Diagnosis: Examine your component tree. Look for the component that is throwing the error. Trace its ancestors. If you find a Suspense component, but no ErrorBoundary component above it (or wrapping it), this is your issue.

    • Fix: Wrap the component that throws the error, or the Suspense boundary itself, with a dedicated ErrorBoundary component.

      import React, { Suspense, ErrorBoundary } from 'react';
      
      const MyComponentThatMightThrow = React.lazy(() => import('./MyComponent'));
      
      function App() {
        return (
          <ErrorBoundary fallback={<ErrorFallback />}>
            <Suspense fallback={<LoadingFallback />}>
              <MyComponentThatMightThrow />
            </Suspense>
          </ErrorBoundary>
        );
      }
      
      function ErrorFallback() {
        return <div>Something went wrong!</div>;
      }
      
      function LoadingFallback() {
        return <div>Loading...</div>;
      }
      
    • Why it works: The ErrorBoundary component has a componentDidCatch or getDerivedStateFromError lifecycle method that is designed to catch errors thrown by its children. When an error occurs within the Suspense boundary, it will bubble up and be caught by this ErrorBoundary.

  2. Error Thrown Before Suspense Kicks In: If an error occurs synchronously during the initial render of a component that is about to be suspended (e.g., a data fetch function that throws immediately, not a promise that rejects later), Suspense won’t have had a chance to render its fallback.

    • Diagnosis: Add console.error statements or use React DevTools to inspect the component that’s failing. If the error logs before you see your LoadingFallback rendering, this is the case.

    • Fix: Ensure your data fetching logic itself is robust. Wrap the call to the data fetching function within a try...catch block if it’s synchronous, or ensure the promise it returns correctly rejects with an error. Then, ensure this logic is within an ErrorBoundary.

      // Inside your component that fetches data
      function MyComponent() {
        try {
          const data = fetchDataSynchronously(); // This might throw
          // ... render with data
        } catch (error) {
          // If fetchDataSynchronously throws, this catch block handles it.
          // But for Suspense to work, the error needs to bubble up to an ErrorBoundary.
          // A better pattern is to return a promise that rejects:
          throw error; // Re-throw to be caught by ErrorBoundary
        }
      }
      
    • Why it works: Even if the error is synchronous, re-throwing it allows it to propagate up the component tree. The ErrorBoundary above is designed to catch these propagated errors, regardless of whether Suspense had already started its loading process.

  3. Incorrect ErrorBoundary Implementation: A custom ErrorBoundary component might be implemented incorrectly, failing to capture errors.

    • Diagnosis: Verify your ErrorBoundary component has either static getDerivedStateFromError(error) or componentDidCatch(error, errorInfo). If it’s a functional component, you’re likely using a library like react-error-boundary. Ensure that library is correctly installed and configured.

    • Fix: If using a class component:

      class ErrorBoundary extends React.Component {
        state = { hasError: false, error: null };
      
        static getDerivedStateFromError(error) {
          return { hasError: true, error };
        }
      
        componentDidCatch(error, errorInfo) {
          console.error("ErrorBoundary caught an error:", error, errorInfo);
          // Log error to an error reporting service
        }
      
        render() {
          if (this.state.hasError) {
            return this.props.fallback; // Render fallback UI
          }
          return this.props.children;
        }
      }
      

      If using react-error-boundary:

      import { ErrorBoundary } from 'react-error-boundary';
      
      function App() {
        return (
          <ErrorBoundary fallbackRender={({ error }) => <div>Error: {error.message}</div>}>
            <Suspense fallback={<LoadingFallback />}>
              <MyComponentThatMightThrow />
            </Suspense>
          </ErrorBoundary>
        );
      }
      
    • Why it works: These methods are specifically designed by React to capture errors during rendering, in lifecycle methods, and in constructors of the whole tree below them.

  4. React.lazy Component Itself Throws Synchronously During Initial Render: While React.lazy is meant for asynchronous loading, the wrapper component it creates can still throw errors synchronously if there’s an issue with its internal setup or if the module it imports has a top-level error.

    • Diagnosis: Look at the stack trace of the uncaught error. If it points to something like React.lazy or the component creation itself, this might be the issue.
    • Fix: Ensure the import() call within React.lazy is valid and the module it points to is correctly structured. More importantly, ensure the React.lazy component is wrapped by an ErrorBoundary. The Suspense boundary will handle the loading state, and the ErrorBoundary will catch any synchronous errors during the initial "render" phase of the lazy component.
    • Why it works: The ErrorBoundary acts as a safety net for any error originating from its children, including those that might arise from the mechanics of React.lazy itself.
  5. Nested Suspense Boundaries Causing Confusion: While valid, deeply nested Suspense boundaries can sometimes mask where an error is truly originating if not paired with a correctly placed ErrorBoundary.

    • Diagnosis: If you have multiple Suspense components in your tree, try simplifying by removing some to see if the error still occurs. Check the error’s stack trace carefully to pinpoint the exact component.
    • Fix: Place a single, high-level ErrorBoundary that encompasses all your Suspense components and their children. This ensures that no matter which Suspense boundary or child component throws, the error is caught.
    • Why it works: A single ErrorBoundary at a higher level simplifies error handling. It doesn’t matter how many loading states are being managed; the error will propagate to the nearest designated error handler.

The Next Error You’ll Hit

If you’ve correctly implemented an ErrorBoundary and are still seeing issues, you might encounter errors related to the fallback UI itself throwing an error, or issues with how state is managed within your ErrorBoundary if you need to re-render it.

Want structured learning?

Take the full React course →