TanStack / TanStack/router

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

Open
#7,078 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
15.1k
Forks
1.9k
Avg merge
1d 20h
Merged PRs (30d)
143

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/react-router/src/ssr/renderRouterToStream.tsx at the renderToPipeableStream callbacks, then reproduce the issue with defaultErrorComponent and a route component that throws. Verify that a recoverable error produces valid fallback HTML with status 500, while an unrecoverable shell error still receives the documented handling.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
backend, web-dev
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.