#4314·swr

Suspense mode passes an uncached promise to use(), triggering React's uncached-promise warning

Author: mrskiroCreated Aug 12, 2026Updated Aug 14, 2026

Bug report

Description / Observed Behavior

In Suspense mode, useSWR calls use() with a promise that is re-created on every render (src/index/use-swr.ts#L875-L885):

typescript
const revalidation =
  hasKeyButNoData && isUndefined(preloadData)
    ? revalidate(WITH_DEDUPE)
    : resolvedUndef
if (!isUndefined(returnedData) && hasKeyButNoData) {
  revalidation.status = 'fulfilled'
  revalidation.value = true
}
use(revalidation)

While there is no data this is a fresh pending promise returned by revalidate(); once data arrives it is the module-level resolvedUndef. React records the thenable passed to use() per hook index and errors when a re-render passes a different instance at the same index:

A component was suspended by an uncached promise. Creating promises inside a Client Component or hook
is not yet supported, except via a Suspense-compatible library or framework.

React's current contract is that the promise handed to use() must be cached by the library ("except via a Suspense-compatible library or framework"), so this surfaces as an SWR-side warning.

Expected Behavior

No warning — the promise handed to use() should be stable across renders for the same key.

Repro Steps / Code Example

Two sibling useSWR(..., { suspense: true }) components under one <Suspense>, with fetchers that resolve immediately:

typescript
const A = () => <p>{useSWR('a', () => Promise.resolve('a'), { suspense: true }).data}</p>
const B = () => <p>{useSWR('b', () => Promise.resolve('b'), { suspense: true }).data}</p>

createRoot(el).render(
  <Suspense fallback={<p>loading</p>}>
    <A />
    <B />
  </Suspense>
)

One uncached promise error is logged, for B.

Why only some hooks hit it

I instrumented react-dom-client.development.js (19.2.8) to log trackUsedThenable and which work-loop path each suspend takes. Trace for the repro above:

-- renderRootSync
   track i=0 status=fulfilled          <- use(resolvedUndef)
   track i=1 status=pending            <- use(revalidate(...)), A suspends
   UNWIND A                            <- throwAndUnwindWorkLoop, resets thenableState
-- renderRootConcurrent
   track i=0 status=fulfilled          <- A re-renders, data present
   track i=1 status=fulfilled
   track i=0 status=fulfilled          <- B mounts
   track i=1 status=pending            <- B suspends
-- renderRootConcurrent
   REPLAY B                            <- replaySuspendedUnitOfWork, keeps thenableState
   track i=0 prev=same
   track i=1 prev=DIFFERENT            <- warning

The deciding factor is which render pass the hook suspends in:

Path Handling of SuspendedOnImmediate thenableState Warning
renderRootSync always throwAndUnwindWorkLoop reset to null no
renderRootConcurrent SuspendedAndReadyToContinue, then replaySuspendedUnitOfWork if the thenable settled before the loop resumes retained yes

So a single suspense hook mounted on the initial (sync) render never warns — it always unwinds and re-renders with a fresh thenable state. A hook that first suspends inside a concurrent render, and whose promise settles before React resumes the yielded loop, hits the replay path and sees a different promise at the same index.

That makes it easy to hit in practice, because mounting inside a Suspense retry counts: in a Next.js 16 app with a suspense useSWR component rendered from another suspense useSWR component's data (both hitting real API routes), the child warned on roughly every other page load — 8 occurrences over 5 reloads.

Additional Context

  • Reproduced on swr 2.4.2 and 2.5.0, react / react-dom 19.2.8.
  • Development builds only — the warning does not exist in React's production build, so this is noise rather than broken behaviour.
  • Caching the promise per key and reusing it until it settles removes the warning in my testing, but I have not checked that approach against the preload / keepPreviousData / RSC cacheData paths.