fix(app-router): preserve global error details#2249
Conversation
|
/bigbonk review for issues |
commit: |
|
Performance benchmarksCompared 0 improved · 0 regressed · 6 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
There was a problem hiding this comment.
Review: fix(app-router): preserve global error details
I verified this change against the authoritative Next.js reference (test/e2e/app-dir/global-error/basic/index.test.ts) and Next.js source (packages/next/src/server/app-render/create-error-handler.tsx). The implementation is correct and matches Next.js behavior in all the cases the PR targets.
Parity verification (confirmed correct)
- RSC server-component error (production): sanitized generic message +
digestpresent. Matches Next.js (Minified React error #441+#digest). ✓ - Client-component SSR throw (production): original
client page errormessage shown, no digest. This is exactly what Next.js does ('Global error: Error: client page error',hasElementByCssSelector('#digest')is falsy). TheerrorOrigin === "ssr"bypass ofsanitizeErrorForClientis the correct way to model this — Next.js does not sanitize SSR client-component errors. ✓ - Unconditional digest stamping (
app-rsc-errors.ts): Next.js stampserr.digest = stringHash(err.message + (err.stack || ''))unconditionally, not gated onNODE_ENV. The previous vinext behavior (production-only) was the divergence; this PR fixes it. The frozen-errortry/catchguard is a sound defensive addition. ✓
Correctness checks (all passed)
getCapturedError() === nullis an exact-null check;capturedErroris initialized tonulland only set to a real captured error, so the"ssr"/"rsc"discrimination is reliable.onRenderError(which stamps the digest) runs during RSC render, beforerenderErrorBoundaryResponsereads the captured error, so RSC errors carry a digest in dev too — consistent with the new dev test.- The other
renderErrorBoundaryPage(error)call sites (app-page-request.ts:506build-element errors,app-page-dispatch.ts:979build errors) correctly fall through to the"rsc"default — those are RSC-origin and should remain sanitized in production. - The digest-stamping change is isolated to the RSC
onErrorpath; thehasDigestdedup inwrapRscResponseForDevErrorReportingconsumes a separateconsumeInvalidDynamicUsageErrorstore and is unaffected. - The global-error→default-fallback path (
renderWith(DEFAULT_GLOBAL_ERROR_COMPONENT)) still works: the default component is wrapped inSerializedErrorBoundaryand receives a reconstructed error, which it accepts.
I ran the targeted unit tests (app-rsc-errors, app-page-boundary-render, app-page-render — 82 passed) and vp check on the changed files (format/lint/types clean).
Minor observations (non-blocking)
-
resetsemantics differ from Next.js.SerializedErrorBoundarypassesreset={() => globalThis.location?.reload()}. Next.js's error/global-errorresetis a soft React state reset (this.setState({ error: null })), not a hard page reload. This is arguably an improvement over the prior behavior (the global-error path previously passed noresetat all, so the button was a no-op), but it's a deliberate divergence worth noting. If exact parity matters, consider routing through the existingErrorBoundaryInner.resetinstead of a full reload. -
"digest" in errorObject ? String(errorObject.digest) : undefinedwould coerce a present-but-undefineddigest to the string"undefined". In practice the sanitized error always sets a defined digest and SSR-origin errors have nodigestkey, so this can't trigger today — but atypeof errorObject.digest === "string"guard would be more defensive.
Neither is blocking. This is solid, well-researched parity work with fixtures ported directly from the Next.js test suite. LGTM.
|
Review posted to PR #2249. SummaryI reviewed the global-error parity fix against the authoritative Next.js reference test ( Key findings (all verified correct):
Validation: ran the 3 targeted unit test files (82 passed) and Two minor non-blocking observations:
No actionable issues — solid parity work with fixtures ported directly from the Next.js suite. |
|
/bigbonk review for issues |
1 similar comment
|
/bigbonk review for issues |
There was a problem hiding this comment.
Review: fix(app-router): preserve global error details
Re-reviewed the full PR (both commits — 1232f14 and the follow-up 4c9650e "preserve default global error semantics", which the prior automated review did not cover) against the authoritative Next.js reference (test/e2e/app-dir/global-error/basic/index.test.ts) and Next.js source (packages/next/src/server/app-render/create-error-handler.tsx). The implementation is correct and matches observable Next.js behavior in all targeted cases. No actionable issues.
Parity verification (confirmed against Next.js source/tests)
- SSR client-component throw → no digest, original message. The reference test asserts
#error='Global error: Error: client page error'andhasElementByCssSelector('#digest')is falsy. TheerrorOrigin === "ssr"bypass ofsanitizeErrorForClient(app-page-boundary-render.ts:435) reproduces this exactly. Note: Next.js'screateHTMLErrorHandlerdoes stamp a digest internally on SSR errors, but the global-error boundary never surfaces it — so "no digest displayed" is the correct observable behavior, and vinext gets there without stamping. ✓ - RSC server-component throw → digest present. Reference asserts
#digestmatches/\w+/, dev shows the original message, prod shows the minified React error. Matches. ✓ - Unconditional digest stamping (
app-rsc-errors.ts:156). Confirmed against Next.jscreateReactServerErrorHandler:err.digestis stamped unconditionally (not gated onNODE_ENV). The prior production-only gate was the divergence; this fixes it. The frozen-errortry/catchis a sound defensive guard and is covered by a test (returns a digest without masking frozen RSC errors). ✓
Second commit (4c9650e) — correct and necessary
Adding BoundaryComponent !== DEFAULT_GLOBAL_ERROR_COMPONENT to the SerializedErrorBoundary wrapping condition is the right call. When the user's global-error.tsx throws and the renderer falls back to renderWith(DEFAULT_GLOBAL_ERROR_COMPONENT) (app-page-boundary-render.ts:570), the built-in default must render directly (mirroring Next.js's direct errorComponent={DefaultGlobalError}) rather than being re-wrapped and routed through a serialization round-trip + location.reload() reset. The self-throw fallback path stays covered by tests/nextjs-compat/global-error.test.ts:393 (single <html>/<body>, 500 status).
Correctness checks (all pass)
getCapturedError() === nulldiscrimination is reliable:capturedErrorinitializes tonull(app-page-stream.ts:344) and is only set for non-navigation-signal RSC errors (:367). SSR throws happen in the separate SSR environment and never touch this tracker, sonull⇒ SSR-origin is sound.- All other
renderErrorBoundaryPage/renderErrorBoundaryResponsecall sites (app-page-request.ts:506build-element errors,app-page-dispatch.ts:979build errors) correctly default to"rsc"— those are server-origin and should stay sanitized in production. app-page-stream.ts:326invokes the discriminating closure fromapp-page-render.ts:871, so the two-arg origin propagation is consistent end-to-end.
Validation
vp test run tests/app-page-boundary-render.test.ts tests/app-page-render.test.ts tests/app-rsc-errors.test.ts→ 82 passedvp test run tests/nextjs-compat/global-error.test.ts→ 24 passedvp checkon all 7 changed source files → format/lint/types clean
Minor observations (non-blocking, same as noted before)
SerializedErrorBoundaryresetis() => globalThis.location?.reload()— a hard reload vs Next.js's soft React state reset. Better than the prior no-op for the global-error path, but a deliberate divergence worth tracking if exactresetparity is ever required."digest" in errorObject ? String(errorObject.digest) : undefinedcould coerce a present-but-undefineddigest to"undefined". Not triggerable today (sanitized errors always set a defined digest; SSR-origin errors have nodigestkey), but atypeof errorObject.digest === "string"guard would be more defensive.
LGTM — solid parity work, fixtures ported directly from the Next.js suite, and the follow-up commit correctly preserves the built-in default fallback semantics.
|
Review posted to PR #2249. SummaryI reviewed the full PR — both commits, including the follow-up Key verifications (confirmed against Next.js source/tests):
Correctness checks passed: Validation I ran: 82 unit tests + 24 nextjs-compat tests passed; Two minor non-blocking observations carried over (hard |
Summary
Next.js parity
Targets the two failures in
test/e2e/app-dir/global-error/basic/index.test.tsfrom Next.js v16.2.6.Validation
vp run vinext#buildpassed