Skip to content

Fix except handler not returning error page for component errors#1086

Merged
justinvdm merged 1 commit intomainfrom
except-issue
Mar 17, 2026
Merged

Fix except handler not returning error page for component errors#1086
justinvdm merged 1 commit intomainfrom
except-issue

Conversation

@justinvdm
Copy link
Copy Markdown
Collaborator

@justinvdm justinvdm commented Mar 17, 2026

Problem

When a server component throws during RSC rendering, the except handler 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 RSC onError callback was mapped to reject() on the outer Promise wrapper. This settled the promise before the except handler could deliver its response via resolve(), 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 subsequent renderPage call.

Solution

Three-part fix:

  1. renderPage captures errors on rw.renderError instead of throwing. When renderDocumentHtmlStream throws AND rw.renderError is set, we catch the throw and return a minimal 500 Response. This keeps renderPage from throwing, avoiding RSC state corruption.

  2. Router checks rw.renderError after renderPage returns. If set, it clears the field and throws, routing to except handlers. The except handler's renderPage call starts with clean state.

  3. pageRouteResolved resolved in error path to prevent the worker from hanging.

The outer new Promise wrapper with onError: reject is removed entirely.

Test plan

  • Added ThrowingComponent server component to kitchen-sink playground (exercises the renderPage/RSC stream path, unlike the existing /debug/throw function handler)
  • Added e2e test "except handler catches errors from component route handlers" — passes consistently (5/5 runs dev+deploy)
  • Reverted fix and confirmed the new test fails without it
  • All 51 router unit tests pass
  • Verified: pnpm test:e2e -- kitchen-sink/__tests__/e2e.test.mts

Fixes #1065

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>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying redwood-sdk-docs with  Cloudflare Pages  Cloudflare Pages

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

View logs

@justinvdm justinvdm merged commit a73bbb6 into main Mar 17, 2026
8 checks passed
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.

Server-side error handling not working as documented

1 participant