All notes

The navigation that never lands.

A lost Suspense ping can leave a click pending forever. How to tell if you are hit, how to patch it and when to retire the patch.

4 min read

A customer presses Pay. The address bar changes. The page does not. There is no error and no spinner that gives up. They wait, press again and it works. The support ticket says the button sometimes does nothing. The logs say everything is fine.

The expensive part is not the bug. It is the week a team spends proving it is real, because nothing fails loudly and a second press hides it.

What was going on

In React 19.2 as bundled with Next 15, a ping that arrived during a render could be lost.

Here is the short version. A navigation or a server action runs as a . When it needs data that is not ready, the render suspends and React waits. When the data arrives, React gets a ping that says try again. If that ping landed while React was in the middle of rendering, it could be dropped. The transition stayed marked as pending and suspended, with nothing left to wake it. The address had already moved. The new page never committed.

Press the button below until one sticks, then turn the patch on.

Try it

Press Pay

Each press starts a navigation. Unpatched, some never commit. Turn the patch on and press again.

/cart

Your cart

Two items

$84.00

The address moved. The page did not.

Presses

0

Landed

0

Stalled

0

The transition lane

pendingsuspendedpingcommit

waiting for a press

A simulation of the pattern, not a real system.

How to tell if you are hit

The signs are consistent once you know them:

  • The URL changes but the view stays on the old page.
  • A useTransition pending flag stays true and never settles.
  • Nothing shows in the browser console or the server logs.
  • It is intermittent and more common when the data is slow.
  • A second press nearly always works.

It can be measured, which beats arguing about it. Mark the moment a navigation or action starts. Mark the moment the destination commits, with an effect in the new page or where the action's result renders. Then count the starts that have no commit a few seconds later. The toy above stalls at the ratio I measured: eleven presses in forty-eight.

If a second press fixes it every time, measure the first press.

Patch it, pinned to the exact version

The fix exists upstream. Until you can upgrade, you can carry it yourself with pnpm patch. Next ships its own copy of React inside the next package, so the patch goes on next, not on react-dom.

Terminal
pnpm patch next@<your exact version># apply the upstream fix to the bundled react-dom in the folder it openspnpm patch-commit <that folder>

That writes a patch file and an entry in package.json keyed to one exact version:

JSON
"pnpm": {  "patchedDependencies": {    "next@<your exact version>": "patches/next.patch"  }}

The discipline is in that key. Never use a range. With an exact version, the next upgrade fails install because the patch no longer matches anything. That is the point. Somebody has to decide on purpose to re-apply the patch to the new version or to retire it. Leave allowUnusedPatches off so pnpm keeps that promise.

For reference, two public pull requests that deal with the same problem: waku #11 (opens in a new tab) and buildd #2816 (opens in a new tab).

Retire it

The fix ships in Next 16.3. Upgrade to 16.3 or later, delete the patch file and remove the patchedDependencies entry. The failed install that follows the upgrade is your reminder.

What happens when this fails?

A patch is code you now own. Three ways it goes wrong:

  • Someone widens the key to a range so an upgrade installs cleanly. The patch then applies to a version it was never tested on or quietly stops applying.
  • Someone turns on unused patches to get a build through. The guard is gone and nobody notices.
  • Nobody keeps measuring. Keep the start and commit counters running after the patch and after the upgrade. Zero stalls is a result you can show. A feeling that it seems better is not.

Next

Here is what I would do next

  • Add the start and commit marks to your busiest navigation and your most important form.
  • If you see starts without commits on Next 15, patch next at its exact version.
  • Plan the upgrade to Next 16.3 or later and retire the patch with it.
  • Keep the counters running for a week after each step.

Have a workflow like this?