The Problem

React Suspense is designed to show a fallback UI while async data loads. But sometimes the fallback never goes away -- the loading spinner spins forever, and the actual content never appears.

Symptoms

  • Loading spinner displays indefinitely
  • No error message in console
  • Network tab shows the request completed successfully
  • The component tree shows Suspense fallback is still active
  • Clicking away and back does not resolve the issue

The Root Cause

Suspense works by catching promises thrown during render. If the promise never resolves, or resolves to something unexpected, Suspense stays in the fallback state forever.

Real Error Scenario

```javascript // BAD: Promise resolves but Suspense doesn't know about it const resource = fetchData();

function Profile() { const user = resource.read(); // Throws promise return <div>{user.name}</div>; }

function App() { return ( <Suspense fallback={<Loading />}> <Profile /> </Suspense> ); } ```

The resource pattern requires a specific implementation. A naive promise wrapper will not work with Suspense.

Common Cause: Incorrect Resource Wrapper

```javascript // WRONG: Just returning a promise doesn't work with Suspense function createResource(promise) { return promise; // Suspense expects a specific API }

// CORRECT: Implement the Suspense resource pattern function createResource(promise) { let status = 'pending'; let result;

const suspender = promise.then( data => { status = 'success'; result = data; }, error => { status = 'error'; result = error; } );

return { read() { if (status === 'pending') throw suspender; if (status === 'error') throw result; return result; } }; } ```

Fix with React 18 use Hook

```javascript import { use } from 'react';

function Profile({ userId }) { const userPromise = fetch(/api/users/${userId}).then(r => r.json()); const user = use(userPromise); // React 18 built-in Suspense support return <div>{user.name}</div>; }

function App() { return ( <Suspense fallback={<Loading />}> <Profile userId={1} /> </Suspense> ); } ```

Fix: Handle Promise Rejection

javascript
function App() {
  return (
    <ErrorBoundary fallback={<ErrorScreen />}>
      <Suspense fallback={<Loading />}>
        <Profile userId={1} />
      </Suspense>
    </ErrorBoundary>
  );
}

If the promise rejects and there's no Error Boundary, Suspense may stay stuck. Always wrap Suspense with an Error Boundary.

Debugging Steps

  1. 1.Open DevTools Console and check for unhandled promise rejections
  2. 2.Check the Network tab -- did the request actually complete?
  3. 3.Add console.log inside the promise resolution to verify it fires
  4. 4.Verify the resource wrapper implements the read() pattern correctly
  5. 5.Check that Suspense wraps the component that throws, not a parent