Which project does this relate to?
Router
Describe the bug
CatchBoundary tracks whether it has caught an error by the truthiness of the thrown value (CatchBoundary.tsx):
static getDerivedStateFromError(error: Error) {
return { error }
}
render() {
const error = this.state.error
if (error) {
// render errorComponent
}
return this.props.children
}
When a component throws a falsy value (throw undefined, throw null, throw ''), the boundary catches it and stores it, but if (error) fails — so it re-renders the same crashing children instead of the errorComponent. React then escalates the error past the boundary. Since every route boundary runs the same check, the error skips every route errorComponent (including the root route's) and the global catch boundary, and the entire tree unmounts. The app crashes with no error UI at all; the value surfaces at createRoot's onUncaughtError.
The reset logic in getDerivedStateFromProps has the same truthiness problem (state.error && state.resetKey !== resetKey).
Nobody throws undefined on purpose, but it happens in real apps (e.g. rethrowing a variable that is unexpectedly undefined). The expected blast radius is the nearest errorComponent; the actual result is a blank page.
Complete minimal reproducer
https://github.com/vladavoX/tsr-catchboundary-falsy-repro
Steps to Reproduce the Bug
The reproducer renders a real RouterProvider (React 19, jsdom — no browser needed) with a route like:
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => { throw undefined }, // vs. throw new Error('boom')
errorComponent: () => <div>ROUTE ERROR BOUNDARY</div>,
})
- Clone the repro repo
npm install
npm start
Output on @tanstack/react-router 1.170.31:
=== component does: throw new Error("boom")
rendered html: <div>ROUTE ERROR BOUNDARY</div>
onUncaughtError: (not called)
=== component does: throw undefined
rendered html: (empty — whole tree unmounted)
onUncaughtError: called with: undefined
=== component does: throw null
rendered html: (empty — whole tree unmounted)
onUncaughtError: called with: null
=== component does: throw '' (empty string)
rendered html: (empty — whole tree unmounted)
onUncaughtError: called with:
Only the throw new Error(...) case renders the route's errorComponent; every falsy throw unmounts the whole tree.
Expected behavior
Any thrown value — falsy included — renders the nearest errorComponent, exactly like throw new Error(...) does. React itself delivers falsy thrown values to getDerivedStateFromError correctly; only the boundary's truthiness check loses them.
Suggested fix — track caught state with a flag instead of the error value's truthiness:
state = { hasError: false, error: null }
static getDerivedStateFromError(error: unknown) {
return { hasError: true, error }
}
// render: if (this.state.hasError) { ... }
// reset: if (state.hasError && state.resetKey !== resetKey) { ... }
Happy to send a PR.
Screenshots or Videos
No response — the console output above shows the behavior.
Platform
- Router / Start Version:
@tanstack/react-router 1.170.31 (bug also present in current main)
- OS: any (verified on macOS 15 / Node 24)
- Browser: any (reproduced in jsdom with React 19; browser-independent)
- Bundler: none (plain Node with
--conditions=browser)
Additional context
Debugged and drafted with the help of Claude (Fable 5), verified by hand against 1.170.31 and current main.
Which project does this relate to?
Router
Describe the bug
CatchBoundarytracks whether it has caught an error by the truthiness of the thrown value (CatchBoundary.tsx):When a component throws a falsy value (
throw undefined,throw null,throw ''), the boundary catches it and stores it, butif (error)fails — so it re-renders the same crashing children instead of theerrorComponent. React then escalates the error past the boundary. Since every route boundary runs the same check, the error skips every routeerrorComponent(including the root route's) and the global catch boundary, and the entire tree unmounts. The app crashes with no error UI at all; the value surfaces atcreateRoot'sonUncaughtError.The reset logic in
getDerivedStateFromPropshas the same truthiness problem (state.error && state.resetKey !== resetKey).Nobody throws
undefinedon purpose, but it happens in real apps (e.g. rethrowing a variable that is unexpectedly undefined). The expected blast radius is the nearesterrorComponent; the actual result is a blank page.Complete minimal reproducer
https://github.com/vladavoX/tsr-catchboundary-falsy-repro
Steps to Reproduce the Bug
The reproducer renders a real
RouterProvider(React 19, jsdom — no browser needed) with a route like:npm installnpm startOutput on
@tanstack/react-router1.170.31:Only the
throw new Error(...)case renders the route'serrorComponent; every falsy throw unmounts the whole tree.Expected behavior
Any thrown value — falsy included — renders the nearest
errorComponent, exactly likethrow new Error(...)does. React itself delivers falsy thrown values togetDerivedStateFromErrorcorrectly; only the boundary's truthiness check loses them.Suggested fix — track caught state with a flag instead of the error value's truthiness:
Happy to send a PR.
Screenshots or Videos
No response — the console output above shows the behavior.
Platform
@tanstack/react-router1.170.31 (bug also present in currentmain)--conditions=browser)Additional context
Debugged and drafted with the help of Claude (Fable 5), verified by hand against 1.170.31 and current
main.