Skip to content

fix(shims): throw Pages Router parity error from unstable_retry (#1448)#1670

Merged
james-elicx merged 1 commit into
mainfrom
fix/issue-1448-unstable-catch-error
May 28, 2026
Merged

fix(shims): throw Pages Router parity error from unstable_retry (#1448)#1670
james-elicx merged 1 commit into
mainfrom
fix/issue-1448-unstable-catch-error

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

Closes the Pages Router parity gap explicitly tracked as a follow-up in #1206, addressing #1448.

When unstable_catchError's boundary is rendered under the Pages Router, calling unstable_retry() previously invoked appRouterInstance.refresh() — a no-op under Pages Router that left the error visible until reset() was called. Next.js documents unstable_retry as App Router only and throws a specific error message from the boundary itself. We now match that behavior verbatim.

  • Inner class component reads RouterContext via static contextType (mirroring Next.js's static contextType = AppRouterContext pattern in client/components/catch-error.tsx).
  • When this.context !== null (Pages Router page), unstable_retry() throws `unstable_retry()` can only be used in the App Router. Use `reset()` in the Pages Router..
  • App Router branch (refresh + reset) is unchanged.
  • Wrapper stays a pure JSX factory so existing introspection-style unit tests that invoke it as a bare function continue to work.

Regression test ported from Next.js' own suite: test/e2e/app-dir/catch-error/catch-error.test.ts — "should throw when unstable_retry is called on Pages Router".

Files

  • packages/vinext/src/shims/error.tsx — wire RouterContext as contextType, branch in unstable_retry, refresh the doc comment.
  • tests/shims.test.ts — new Pages Router branch test; seed this.context = null on the two existing App Router tests (manual class instantiation skips React's context wiring).

Test plan

  • pnpm test tests/shims.test.ts -t "unstable_catchError" — 10/10 pass.
  • pnpm test tests/shims.test.ts tests/error-boundary.test.ts — 1046/1046 pass.
  • pnpm run check — clean (format + lint + type).
  • CI Vitest / Playwright E2E (will run on PR).

Out of scope (documented follow-ups remaining from #1206)

  • Server Component error recovery via unstable_retry (requires Suspense-boundary re-fetch wiring).
  • Bot-user-agent graceful-degradation, handleHardNavError, handleISRError.
  • Per-environment shim split (react-server throwing stub vs client implementation).

Closes the Pages Router parity gap noted as a follow-up in #1206. When
`unstable_catchError`'s boundary is rendered under the Pages Router,
`unstable_retry()` now throws Next.js's verbatim error message
("`unstable_retry()` can only be used in the App Router. Use `reset()`
in the Pages Router.") instead of calling App Router's `refresh()`
(which is a no-op there and leaves the user stuck on the error).

Detection mirrors Next.js's own implementation in
`packages/next/src/client/components/catch-error.tsx`: the inner class
component reads `RouterContext` via `static contextType`. The wrapper
stays a pure JSX factory so existing introspection-style unit tests
that invoke it as a bare function continue to work.

Regression test ported from
`test/e2e/app-dir/catch-error/catch-error.test.ts` —
"should throw when unstable_retry is called on Pages Router".
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review

@pkg-pr-new

pkg-pr-new Bot commented May 28, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1670

commit: 17e6ec3

@github-actions

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
web preview production

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(shims): throw Pages Router parity error from unstable_retry

Looks good. The implementation is correct and the tests cover the important branches. A few observations:

Correctness

The context detection logic is sound. The approach is inverted relative to Next.js (vinext reads RouterContext and checks !== null for Pages Router, while Next.js reads AppRouterContext and checks === null for "not App Router"), but the end result is equivalent:

  • RouterContext defaults to null via createContext<NextRouter | null>(null)
  • Pages Router pages are always wrapped in RouterContext.Provider by wrapWithRouterContext() (shims/router.ts:1613)
  • Under App Router, no RouterContext.Provider exists → this.context stays null
  • So this.context !== null ↔ "we're under Pages Router" ✓

This approach is actually slightly cleaner for vinext because AppRouterContext can itself be null at the module level (if React.createContext is unavailable), which would add an extra edge case.

Test fixes for existing tests

The seeding of (instance).context = null in the two existing App Router tests is correct and necessary. Without it, this.context would be undefined (manual new skips React's context wiring), and undefined !== null is true, which would incorrectly trigger the Pages Router error path. Good catch.

New test

The Pages Router regression test seeds this.context = { route: "/some-page" } and asserts the verbatim error message. This matches the expected production behavior where React fills this.context from RouterContext.Provider.

Minor note

The void React on line 1406 of the test exists solely to suppress the "unused import" lint. It's a pattern used in sibling tests — fine to keep for consistency.

LGTM.

@ask-bonk

ask-bonk Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Review posted. The PR is approved — the implementation correctly detects Pages Router via RouterContext as contextType, the !== null guard properly distinguishes Pages Router (context populated) from App Router (context stays default null), and the test coverage addresses both the new Pages Router branch and fixes the existing App Router tests to seed context = null for manual class instantiation.

github run

@james-elicx
james-elicx marked this pull request as ready for review May 28, 2026 16:54
@james-elicx
james-elicx merged commit a6a2795 into main May 28, 2026
38 checks passed
@james-elicx
james-elicx deleted the fix/issue-1448-unstable-catch-error branch May 28, 2026 20:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant