Skip to content

Add unstable_retry() to error.js#89685

Merged
devjiwonchoi merged 12 commits intocanaryfrom
jiwon/02-08-add_retry_logic_for_error.js
Feb 26, 2026
Merged

Add unstable_retry() to error.js#89685
devjiwonchoi merged 12 commits intocanaryfrom
jiwon/02-08-add_retry_logic_for_error.js

Conversation

@devjiwonchoi
Copy link
Member

@devjiwonchoi devjiwonchoi commented Feb 8, 2026

Extends the error components API to give better control over recovery. Previously, the reset() prop only cleared the error state and re-rendered the children. However, this only handles a temporary rendering error.

The error can be due to data fetching or an RSC phase. In these cases, reset() alone is not sufficient. Users would even need to implement retry logic using router.refresh().

Therefore, this PR adds a new unstable_retry() prop that calls router.refresh() and reset() within a startTransition() to provide built-in retry logic. This feature is expected to be preferred over the reset() prop. Only the cases where you'd choose reset() are when you have a reason to do a sync reset without loading any new data.

Closes NAR-767

Copy link
Member Author

devjiwonchoi commented Feb 8, 2026

@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Feb 8, 2026

Tests Passed

@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Feb 8, 2026

Stats from current PR

✅ No significant changes detected

📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 915ms 964ms ▁█▁▂▁
Cold (Ready in log) 920ms 924ms ▁█▁▂▂
Cold (First Request) 1.713s 1.725s ▁█▁▃▃
Warm (Listen) 913ms 964ms ▁█▁▂▁
Warm (Ready in log) 917ms 922ms ▁█▁▂▁
Warm (First Request) 701ms 676ms ▁█▁▃▁
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 456ms 456ms ▁█▁▁█
Cold (Ready in log) 451ms 449ms ▅▇▅▁█
Cold (First Request) 2.087s 2.078s ▃▆▄▁█
Warm (Listen) 457ms 456ms ▁█▁▁█
Warm (Ready in log) 449ms 451ms ▄▆▃▁█
Warm (First Request) 2.093s 2.091s ▃▆▃▁█

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 6.777s 6.678s ▁█▁▃▁
Cached Build 6.833s 6.657s ▁█▁▃▁
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 15.132s 15.073s ▁▇▂▁█
Cached Build 15.198s 15.160s ▁▇▁▁█
node_modules Size 475 MB 475 MB ▁▁▁▁▁
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles: **400 kB** → **400 kB** ⚠️ +48 B

80 files with content-based hashes (individual files not comparable between builds)

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 764 B 764 B
Total 764 B 764 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 451 B 452 B
Total 451 B 452 B ⚠️ +1 B

📦 Webpack

Client

Main Bundles
Canary PR Change
5528-HASH.js gzip 5.54 kB N/A -
6280-HASH.js gzip 58.3 kB N/A -
6335.HASH.js gzip 169 B N/A -
912-HASH.js gzip 4.59 kB N/A -
e8aec2e4-HASH.js gzip 62.6 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 255 B 254 B
main-HASH.js gzip 39.1 kB 39.1 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
262-HASH.js gzip N/A 4.59 kB -
2889.HASH.js gzip N/A 169 B -
5602-HASH.js gzip N/A 5.55 kB -
6948ada0-HASH.js gzip N/A 62.6 kB -
9544-HASH.js gzip N/A 59.1 kB -
Total 232 kB 233 kB ⚠️ +782 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 194 B 194 B
_error-HASH.js gzip 183 B 180 B 🟢 3 B (-2%)
css-HASH.js gzip 331 B 330 B
dynamic-HASH.js gzip 1.81 kB 1.81 kB
edge-ssr-HASH.js gzip 256 B 256 B
head-HASH.js gzip 351 B 352 B
hooks-HASH.js gzip 384 B 383 B
image-HASH.js gzip 580 B 581 B
index-HASH.js gzip 260 B 260 B
link-HASH.js gzip 2.5 kB 2.5 kB
routerDirect..HASH.js gzip 320 B 319 B
script-HASH.js gzip 386 B 386 B
withRouter-HASH.js gzip 315 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.97 kB 7.97 kB ✅ -2 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 125 kB 125 kB
page.js gzip 254 kB 255 kB
Total 379 kB 379 kB ⚠️ +415 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 616 B 617 B
middleware-r..fest.js gzip 156 B 155 B
middleware.js gzip 43.9 kB 44 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 45.5 kB 45.6 kB ⚠️ +88 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 715 B 718 B
Total 715 B 718 B ⚠️ +3 B
Build Cache
Canary PR Change
0.pack gzip 4.01 MB 4.03 MB 🔴 +14 kB (+0%)
index.pack gzip 102 kB 102 kB
index.pack.old gzip 103 kB 102 kB
Total 4.22 MB 4.23 MB ⚠️ +13.4 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 320 kB 320 kB
app-page-exp..prod.js gzip 170 kB 170 kB
app-page-tur...dev.js gzip 319 kB 319 kB
app-page-tur..prod.js gzip 169 kB 169 kB
app-page-tur...dev.js gzip 316 kB 316 kB
app-page-tur..prod.js gzip 168 kB 168 kB
app-page.run...dev.js gzip 316 kB 316 kB
app-page.run..prod.js gzip 168 kB 168 kB
app-route-ex...dev.js gzip 70.8 kB 70.8 kB
app-route-ex..prod.js gzip 49.2 kB 49.2 kB
app-route-tu...dev.js gzip 70.8 kB 70.8 kB
app-route-tu..prod.js gzip 49.2 kB 49.2 kB
app-route-tu...dev.js gzip 70.4 kB 70.4 kB
app-route-tu..prod.js gzip 49 kB 49 kB
app-route.ru...dev.js gzip 70.4 kB 70.4 kB
app-route.ru..prod.js gzip 49 kB 49 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 43.2 kB 43.2 kB
pages-api-tu..prod.js gzip 32.9 kB 32.9 kB
pages-api.ru...dev.js gzip 43.2 kB 43.2 kB
pages-api.ru..prod.js gzip 32.8 kB 32.8 kB
pages-turbo....dev.js gzip 52.5 kB 52.5 kB
pages-turbo...prod.js gzip 38.5 kB 38.5 kB
pages.runtim...dev.js gzip 52.5 kB 52.5 kB
pages.runtim..prod.js gzip 38.4 kB 38.4 kB
server.runti..prod.js gzip 61.9 kB 61.9 kB
Total 2.82 MB 2.82 MB ⚠️ +664 B
📝 Changed Files (8 files)

Files with changes:

  • app-page-exp..ntime.dev.js
  • app-page-exp..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page-tur..ntime.dev.js
  • app-page-tur..time.prod.js
  • app-page.runtime.dev.js
  • app-page.runtime.prod.js
View diffs
app-page-exp..ntime.dev.js
failed to diff
app-page-exp..time.prod.js

Diff too large to display

app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js

Diff too large to display

app-page-tur..ntime.dev.js
failed to diff
app-page-tur..time.prod.js
failed to diff
app-page.runtime.dev.js
failed to diff
app-page.runtime.prod.js

Diff too large to display

📎 Tarball URL
next@https://vercel-packages.vercel.app/next/prs/89685/next

@devjiwonchoi devjiwonchoi force-pushed the jiwon/02-08-add_retry_logic_for_error.js branch 2 times, most recently from 4bc9128 to 3cd7ff9 Compare February 9, 2026 01:17
@devjiwonchoi devjiwonchoi changed the title Add retry, componentStack, and ownerStack to error.js Add retry(), componentStack, and ownerStack to error.js Feb 9, 2026
@devjiwonchoi devjiwonchoi force-pushed the jiwon/02-08-add_retry_logic_for_error.js branch from 3cd7ff9 to e5e0973 Compare February 9, 2026 22:06
@devjiwonchoi devjiwonchoi force-pushed the jiwon/02-08-allow_reset_from_user_s_global-error branch from 564cf7e to 2a614e7 Compare February 9, 2026 22:06
@devjiwonchoi devjiwonchoi force-pushed the jiwon/02-08-add_retry_logic_for_error.js branch from f47659f to bd6fd7a Compare February 10, 2026 00:07
@devjiwonchoi devjiwonchoi marked this pull request as ready for review February 11, 2026 14:02
@devjiwonchoi devjiwonchoi force-pushed the jiwon/02-08-add_retry_logic_for_error.js branch from bd6fd7a to b8e507c Compare February 11, 2026 16:49
@devjiwonchoi devjiwonchoi force-pushed the jiwon/02-08-allow_reset_from_user_s_global-error branch 2 times, most recently from 1f6797f to 7e8af59 Compare February 15, 2026 02:27
@devjiwonchoi devjiwonchoi force-pushed the jiwon/02-08-add_retry_logic_for_error.js branch from b8e507c to f6e1243 Compare February 15, 2026 02:27
@devjiwonchoi devjiwonchoi changed the title Add retry(), componentStack, and ownerStack to error.js Add unstable_retry(), componentStack, and ownerStack to error.js Feb 24, 2026
@devjiwonchoi devjiwonchoi force-pushed the jiwon/02-08-add_retry_logic_for_error.js branch from 40c132c to 29a7740 Compare February 25, 2026 06:52
@devjiwonchoi devjiwonchoi added the CI Bypass Graphite Optimization Ignore Graphite CI optimizations, run the full CI suite. https://graphite.dev/docs/stacking-and-ci label Feb 25, 2026
@devjiwonchoi devjiwonchoi force-pushed the jiwon/02-08-add_retry_logic_for_error.js branch from 29a7740 to 93d8795 Compare February 25, 2026 07:44
@nextjs-bot nextjs-bot added create-next-app Related to our CLI tool for quickly starting a new Next.js application. Documentation Related to Next.js' official documentation. examples Issue was opened via the examples template. Font (next/font) Related to Next.js Font Optimization. labels Feb 25, 2026
@nextjs-bot nextjs-bot added the Turbopack Related to Turbopack with Next.js. label Feb 25, 2026
@devjiwonchoi devjiwonchoi force-pushed the jiwon/02-08-add_retry_logic_for_error.js branch from 93d8795 to 24b79b2 Compare February 25, 2026 07:54
@devjiwonchoi devjiwonchoi removed examples Issue was opened via the examples template. create-next-app Related to our CLI tool for quickly starting a new Next.js application. Font (next/font) Related to Next.js Font Optimization. Documentation Related to Next.js' official documentation. Turbopack Related to Turbopack with Next.js. labels Feb 25, 2026
)
}

let hasThrown = false
Copy link
Contributor

Choose a reason for hiding this comment

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

It should probably set a globalThis to false and then in the test itself you set it to true to switch from erroring to recovered. by using a render side effect the test could change subtly in the future based on multiple renders that might make it no longer assert what we expect

Copy link
Member Author

Choose a reason for hiding this comment

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

Next test spins up a new process, so handling globalThis inside suites is not shared between the Jest worker, so I used a search query and a route to enable/disable the "should throw" value.

).toMatchInlineSnapshot(`
[
"at ErrorBoundary",
"at OuterLayoutRouter",
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't the owner stack we want. we want the owner stack of the component that errored. We should talk to Sebbie about where to call this so we get the right stack

Copy link
Member Author

Choose a reason for hiding this comment

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

As discussed, splitted from this PR, should follow up

devjiwonchoi added a commit that referenced this pull request Feb 26, 2026
The module-scoped `hasThrown` variable flipped during render, which is
fragile if React re-renders multiple times. Use a `globalThis` flag
toggled via a route handler so the test explicitly controls recovery.

#89685 (comment)
devjiwonchoi added a commit that referenced this pull request Feb 26, 2026
…errors

Add Container wrapper around the throwing server component so
componentStack and ownerStack reflect a realistic hierarchy. Render
componentStack/ownerStack in the server-component error boundary and
add inline snapshot assertions.

#89685 (comment)
@devjiwonchoi devjiwonchoi changed the title Add unstable_retry(), componentStack, and ownerStack to error.js Add unstable_retry() to error.js Feb 26, 2026
Copy link
Contributor

@gnoff gnoff left a comment

Choose a reason for hiding this comment

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

I think you should keep the parent stack thought you could land separately

Base automatically changed from jiwon/02-08-allow_reset_from_user_s_global-error to canary February 26, 2026 19:56
Extends the error components API to give better control over recovery.
Previously, the `reset` prop only cleared the error state and re-rendered
the children. However, this only handles a temporary error during rendering.

The error can be from data fetching, or from a RSC phase etc. So in these
cases `reset` alone is not enough. The users would even need to implement
a retry logic with `router.refresh()`.

Therefore this commit adds a new `retry` prop which calls `router.refresh()`
and `reset` inside a `startTransition` to provide a built-in retry logic.
This feature is expected to preferred over than the `reset` prop. Only the
cases where you'd choose `reset` is when you have a reason to do sync reset
without loading any new data.

This commit also adds `componentStack` and `ownerStack` (dev-only) to the
error components API for better DX and debugging error handling.
error-boundary.tsx imported publicAppRouterInstance from app-router-instance.ts, which transitively pulls in the entire App Router module graph (router-reducer → fetch-server-response →
  react-server-dom-webpack/client). This broke any non-App-Router context (e.g. Pages Router) that imports a module depending on error-boundary.tsx, since react-server-dom-webpack/client is
  unavailable there. Replace with AppRouterContext from the lightweight shared-runtime module, which only contains type imports and React context creation — no heavy runtime dependencies. The error
  boundary already renders inside the AppRouterContext.Provider scope, so this.context always has the router instance for segment-level boundaries.
The module-scoped `hasThrown` variable flipped during render, which is
fragile if React re-renders multiple times. Use a `globalThis` flag
toggled via a route handler so the test explicitly controls recovery.

#89685 (comment)
…errors

Add Container wrapper around the throwing server component so
componentStack and ownerStack reflect a realistic hierarchy. Render
componentStack/ownerStack in the server-component error boundary and
add inline snapshot assertions.

#89685 (comment)
The ownerStack captured in the error boundary returns the error
boundary's owner stack instead of the errored component's.
Remove componentStack and ownerStack from error.js props to
address them separately with a correct capture strategy.
CI no longer produces ClientPageRoot in the redbox stack trace.
@devjiwonchoi devjiwonchoi force-pushed the jiwon/02-08-add_retry_logic_for_error.js branch from 65c45ec to c858df2 Compare February 26, 2026 20:01
@devjiwonchoi devjiwonchoi merged commit 9d13b67 into canary Feb 26, 2026
157 checks passed
@devjiwonchoi devjiwonchoi deleted the jiwon/02-08-add_retry_logic_for_error.js branch February 26, 2026 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI Bypass Graphite Optimization Ignore Graphite CI optimizations, run the full CI suite. https://graphite.dev/docs/stacking-and-ci created-by: Next.js team PRs by the Next.js team. tests type: next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants