Fix except handler not returning error page for component errors#1086
Merged
Fix except handler not returning error page for component errors#1086
Conversation
When a server component throws during RSC rendering, React's onError
callback fired and was mapped to reject() on the outer Promise wrapper
in defineApp. This settled the promise before the except handler could
produce its response, discarding the error page.
The fix has three parts:
1. renderPage now captures rendering errors on rw.renderError (a new
field on RwContext) instead of letting them propagate as thrown
exceptions. Throwing mid-render corrupts React's internal RSC stream
state ("chunk.reason.enqueueModel is not a function"), preventing
subsequent renders from working.
2. When renderDocumentHtmlStream throws AND an error was already
captured via onError, renderPage catches the throw and returns a
minimal 500 Response (instead of re-throwing). This preserves clean
React state for the except handler's render.
3. The router checks rw.renderError after renderPage returns and throws
it, routing to except handlers. pageRouteResolved is also resolved
in the error path to prevent the worker from hanging.
The outer Promise wrapper with onError: reject is removed entirely.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Deploying redwood-sdk-docs with
|
| Latest commit: |
c580244
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://b46c9bbe.redwood-sdk-docs.pages.dev |
| Branch Preview URL: | https://except-issue.redwood-sdk-docs.pages.dev |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When a server component throws during RSC rendering, the
excepthandler runs (the user sees console.error output in the terminal), but the error page is never returned to the browser. Instead, a Vite HMR error overlay is shown in dev, or a generic 500 in production.The root cause is a race condition in
defineApp(worker.tsx): React's RSConErrorcallback was mapped toreject()on the outer Promise wrapper. This settled the promise before the except handler could deliver its response viaresolve(), discarding the error page.A secondary issue is that throwing from
renderPage(even after catching the error) corrupts React's internal RSC flight client state (chunk.reason.enqueueModel is not a function), preventing the except handler from rendering its error page in a subsequentrenderPagecall.Solution
Three-part fix:
renderPagecaptures errors onrw.renderErrorinstead of throwing. WhenrenderDocumentHtmlStreamthrows ANDrw.renderErroris set, we catch the throw and return a minimal 500 Response. This keepsrenderPagefrom throwing, avoiding RSC state corruption.Router checks
rw.renderErrorafterrenderPagereturns. If set, it clears the field and throws, routing to except handlers. The except handler'srenderPagecall starts with clean state.pageRouteResolvedresolved in error path to prevent the worker from hanging.The outer
new Promisewrapper withonError: rejectis removed entirely.Test plan
ThrowingComponentserver component to kitchen-sink playground (exercises therenderPage/RSC stream path, unlike the existing/debug/throwfunction handler)pnpm test:e2e -- kitchen-sink/__tests__/e2e.test.mtsFixes #1065