Summary
createContainer is called with the pre-0.31 (React 18-era) argument list, but the package depends on react-reconciler@^0.31.0. Two of the three error callbacks land in the wrong slots and onCaughtError ends up null.
The practical effect: any error caught by an <ErrorBoundary> crashes the process with a misleading TypeError: onCaughtError is not a function, instead of being reported. onRecoverableError is also never wired.
Current call
src/reconciler/render.ts (dist/reconciler/render.js:82):
const container = TuiReconciler.createContainer(root, 0, null, false, null, "", onRecoverableError, null);
That's 8 arguments. The 0.31 signature takes 10:
exports.createContainer = function (
containerInfo, tag, hydrationCallbacks, isStrictMode,
concurrentUpdatesByDefaultOverride, identifierPrefix,
onUncaughtError, onCaughtError, onRecoverableError, transitionCallbacks
) { … }
So currently:
| slot |
gets |
should get |
onUncaughtError |
onRecoverableError |
uncaught handler |
onCaughtError |
null |
caught handler |
onRecoverableError |
undefined |
recoverable handler |
The uncaught path works by luck — storm's handler happens to land in the right slot. The caught path is null, so logCaughtError's root.onCaughtError(...) throws.
src/reconciler/render-to-string.ts (dist/reconciler/render-to-string.js:71) has the same 8-arg call, which means renderToString silently fails to collect errors from error boundaries and recoverable errors.
Reproduction
npm install @orchetron/storm@0.2.0 react@19
// repro.tsx — any error inside an ErrorBoundary will do
import { render, Box, Text, ErrorBoundary } from "@orchetron/storm";
import React from "react";
function Boom(): React.ReactNode {
throw new Error("boom");
}
render(
<ErrorBoundary fallback={(err) => <Text>caught: {err.message}</Text>}>
<Box><Boom /></Box>
</ErrorBoundary>,
);
The fallback paints caught: boom, and then:
Uncaught exception: TypeError: onCaughtError is not a function
at logCaughtError (…/react-reconciler/cjs/react-reconciler.development.js:5524:9)
at runWithFiberInDEV (…/react-reconciler/cjs/react-reconciler.development.js:522:16)
at inst.componentDidCatch.update.callback (…/react-reconciler/cjs/react-reconciler.development.js:5571:11)
at callCallback (…/react-reconciler/cjs/react-reconciler.development.js:2516:16)
…
…/@orchetron/storm/dist/core/screen.js:77
throw err;
^
Screen.onUncaughtException rethrows, so the process dies. Because this fires from the error boundary's own commit, it replaces whatever the real error was — the boundary renders its fallback for a frame and then the process exits, so the underlying error is effectively invisible. That's how it presented for me: a genuine bug in my tree (#21) was completely masked by this.
Suggested fix
const onCaughtError = (err) => {
if (options.onError) options.onError(err);
};
const container = TuiReconciler.createContainer(
root, 0, null, false, null, "",
onRecoverableError, // onUncaughtError
onCaughtError, // onCaughtError
onRecoverableError, // onRecoverableError
null,
);
Judgement call worth your input: I deliberately did not route onCaughtError to the default stderr writer. An <ErrorBoundary> that caught an error is already showing its fallback, so writing a raw stack to stderr scribbles over the alt screen. Forwarding only to a user-supplied options.onError seemed right, but you may prefer always logging, or adding a separate option.
For render-to-string.ts the fix is simpler — the same collector in all three slots:
const collect = (error) => { errors.push(error); };
const container = TuiReconciler.createContainer(root, 0, null, false, null, "", collect, collect, collect, null);
Possibly related: peer range
package.json declares:
"peerDependencies": { "react": "^18.0.0 || ^19.0.0" },
"dependencies": { "react-reconciler": "^0.31.0" }
but react-reconciler@0.31.0 itself declares "peerDependencies": { "react": "^19.0.0" }. The ^18.0.0 half of that range can't work regardless of this fix — worth narrowing to ^19.0.0 so React 18 users fail at install rather than at runtime. I haven't actually tried it on React 18, so I may be missing something.
Environment
@orchetron/storm 0.2.0 (latest on npm)
react 19.2.8, react-reconciler 0.31.0
- Node 24.15.0, Linux
Summary
createContaineris called with the pre-0.31 (React 18-era) argument list, but the package depends onreact-reconciler@^0.31.0. Two of the three error callbacks land in the wrong slots andonCaughtErrorends upnull.The practical effect: any error caught by an
<ErrorBoundary>crashes the process with a misleadingTypeError: onCaughtError is not a function, instead of being reported.onRecoverableErroris also never wired.Current call
src/reconciler/render.ts(dist/reconciler/render.js:82):That's 8 arguments. The 0.31 signature takes 10:
So currently:
onUncaughtErroronRecoverableErroronCaughtErrornullonRecoverableErrorundefinedThe uncaught path works by luck — storm's handler happens to land in the right slot. The caught path is
null, sologCaughtError'sroot.onCaughtError(...)throws.src/reconciler/render-to-string.ts(dist/reconciler/render-to-string.js:71) has the same 8-arg call, which meansrenderToStringsilently fails to collect errors from error boundaries and recoverable errors.Reproduction
The fallback paints
caught: boom, and then:Screen.onUncaughtExceptionrethrows, so the process dies. Because this fires from the error boundary's own commit, it replaces whatever the real error was — the boundary renders its fallback for a frame and then the process exits, so the underlying error is effectively invisible. That's how it presented for me: a genuine bug in my tree (#21) was completely masked by this.Suggested fix
Judgement call worth your input: I deliberately did not route
onCaughtErrorto the default stderr writer. An<ErrorBoundary>that caught an error is already showing its fallback, so writing a raw stack to stderr scribbles over the alt screen. Forwarding only to a user-suppliedoptions.onErrorseemed right, but you may prefer always logging, or adding a separate option.For
render-to-string.tsthe fix is simpler — the same collector in all three slots:Possibly related: peer range
package.jsondeclares:but
react-reconciler@0.31.0itself declares"peerDependencies": { "react": "^19.0.0" }. The^18.0.0half of that range can't work regardless of this fix — worth narrowing to^19.0.0so React 18 users fail at install rather than at runtime. I haven't actually tried it on React 18, so I may be missing something.Environment
@orchetron/storm0.2.0 (latest on npm)react19.2.8,react-reconciler0.31.0