Skip to content

onError in renderRouterToStream destroys stream even for recoverable errors, breaking Error Boundary SSR rendering #7078

@myamaak

Description

@myamaak

Which project does this relate to?

Start

Describe the bug

In renderRouterToStream.tsx, the onError callback added in #5996 destroys the PassThrough stream on any error, including errors that are caught by React Error Boundaries (recoverable errors).

React's renderToPipeableStream calls onError for all errors — both recoverable (caught by Error Boundary) and unrecoverable. When the error is recoverable:

  1. Error Boundary catches the error and renders errorComponent / defaultErrorComponent
  2. onShellReady fires normally — the shell includes the error fallback UI
  3. onError also fires → stream.destroy() kills the already-piping stream
  4. Response body becomes broken → upstream proxy returns 502 Bad Gateway

This means defaultErrorComponent and route-level errorComponent never render during SSR — users always see a 502 instead of the intended error page.

Your Example Website or App

N/A (reproducible in any TanStack Start app with defaultErrorComponent set)

Steps to Reproduce the Bug or Issue

  1. Set defaultErrorComponent in createRouter():
const router = createRouter({
    routeTree,
    defaultErrorComponent: () => <MyErrorPage />,
})
  1. Throw an error in any route component:
function RouteComponent() {
    throw new Error('test error')
    return <div>Hello</div>
}
  1. Access the page via SSR → 502 Bad Gateway instead of MyErrorPage

Expected behavior

The Error Boundary should catch the error and render defaultErrorComponent during SSR, returning a valid HTML response (with status 500).

Screenshots or Videos

No response

Platform

@tanstack/react-router: 1.167.16 (any version after #5996 merge on 2025-11-29)
@tanstack/react-start: 1.167.16

Additional context

Root Cause

In packages/react-router/src/ssr/renderRouterToStream.tsx:

onError: (error, info) => {
    console.error('Error in renderToPipeableStream:', error, info)
    // Destroy the passthrough stream on error
    if (!reactAppPassthrough.destroyed) {
        reactAppPassthrough.destroy(
            error instanceof Error ? error : new Error(String(error)),
        )
    }
},

Proposed Fix

Follow React's recommended pattern:

onShellError(error) {
  console.error('SSR Shell Error:', error)
  if (!reactAppPassthrough.destroyed)
    reactAppPassthrough.destroy(
      error instanceof Error ? error : new Error(String(error)),
    )
},
onError: (error, info) => {
  console.error('Error in renderToPipeableStream:', error, info)
},
  • onError: logging/flag-setting only, no stream.destroy().
    This aligns with React's recommended pattern
    where onError is used solely for logging — none of React's official
    examples destroy the stream in onError.
  • onShellError (new): handle unrecoverable shell errors with fallback HTML,
    as documented in React's API reference.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions