Integrations
Solid
Solid is the one framework on this list whose errors reach you through a
function you call rather than a callback you hand over, and that single
difference decides most of what follows: onError and <ErrorBoundary> are
alternatives, not complements, and neither of them sees an error thrown in
an event handler. Every claim below was read out of [email protected] in this
repository's node_modules — the source, not the documentation, which is
silent on two of the four things that matter.
The integration #
// App.tsx — the component that wraps the app
import { ErrorBoundary, onError } from "solid-js";
import { mountBugbottle } from "bugbottle/ui";
import { htmlToImage } from "bugbottle/html-to-image"; // optional
import { da } from "bugbottle/locales";
const widget = mountBugbottle({
endpoint: "/api/feedback",
screenshot: htmlToImage,
locale: da,
openOnError: { prefill: true },
});
export default function App(props: { children?: JSX.Element }) {
// Registered in the component, never at module scope. See below.
onError((error) => {
widget.open();
// Keep the console. A handler is not a logger; it swallows the error.
console.error(error);
});
return <ErrorBoundary fallback={(error) => <Crash error={error} />}>{props.children}</ErrorBoundary>;
}onError sits in the component and not in the module because of the first trap.
It catches render errors, and handleError reads the handler list off the owner
it is called with — so it covers the component it is called in and the
computations created under it, and nothing outside. A handler in the component
that threw is too late; one at the top of the tree is right.
ErrorBoundary has no onerror prop, and it takes onError's place #
// node_modules/solid-js/dist/solid.js
function ErrorBoundary(props) {
let err;
if (sharedConfig.context && sharedConfig.load) err = sharedConfig.load(sharedConfig.getContextId());
const [errored, setErrored] = createSignal(err, undefined);
...
return createMemo(() => {
let e;
if (e = errored()) {
const f = props.fallback;
return typeof f === "function" && f.length ? untrack(() => f(e, () => setErrored())) : f;
}
return catchError(() => props.children, setErrored);
}, undefined, undefined);
}The props are fallback and children. There is no onerror to pass, and the
handler the boundary installs for its own subtree is setErrored — it stores
the error and renders your fallback, which is the whole job. Two details worth
knowing: the fallback is called with (error, reset) only if it declares
parameters (f.length is the test), so fallback={() => <p>Broken</p>} is
rendered as a value rather than called; and catchError builds a fresh
context for the subtree,
// node_modules/solid-js/dist/solid.js
function catchError(fn, handler) {
ERROR || (ERROR = Symbol("error"));
Owner = createComputation(undefined, undefined, true);
Owner.context = { ...Owner.context, [ERROR]: [handler] };so the boundary replaces the inherited onError list for everything inside
it. Put onError above an ErrorBoundary expecting to be told about errors
inside it and nothing happens: the boundary's setErrored is the only handler
in that context, and the error never leaves. Pick the one you want — a report
per caught error (onError, no boundary, the integration above) or a fallback
that renders (ErrorBoundary, and read the error out of the render prop).
onError outside a component is discarded without a word #
// node_modules/solid-js/dist/solid.js
function onError(fn) {
ERROR || (ERROR = Symbol("error"));
if (Owner === null) ;else if (Owner.context === null || !Owner.context[ERROR]) {The first branch is an empty statement. Call onError at module scope, in a
createRoot callback that has already returned, or in a helper invoked from
outside the component tree, and the handler is dropped: no warning, no error,
and the error goes to the window instead. This is the Solid version of
Svelte's boundary with only a pending snippet,
and it is quieter — that one at least reaches the console.
An error in an event handler reaches neither of them #
// node_modules/solid-js/web/dist/web.js
const handleNode = () => {
const handler = node[key];
if (handler && !node.disabled) {
const data = node[`${key}Data`];
data !== undefined ? handler.call(node, data, e) : handler.call(node, e);delegateEvents registers one eventHandler per event name on the document,
and it calls your handler directly: no runUpdates wrapper, no owner set.
The non-delegated path is the same — addEventListener wraps the listener in
e => handlerFn.call(node, handler[1], e). So a throw inside an onClick
leaves the DOM listener through the browser's own error path, which means
ErrorBoundary never sees it and onError never runs. The only thing that
catches it is the window.onerror patch
initConsoleBuffer already does, which is
why that call is not optional in a Solid application: it is the one part of the
error path a boundary does not cover. onUncaughtError is not a Solid hook, so
there is nothing to wire in its place.
What arrives is not what you threw #
// node_modules/solid-js/dist/solid.js
function castError(err) {
if (err instanceof Error) return err;
return new Error(typeof err === "string" ? err : "Unknown error", { cause: err });
}An Error arrives as itself. Anything else is replaced by a new Error
carrying the original as cause — and that new error's stack is created
inside solid-js, so a report built from a thrown object or a thrown string
carries Solid's frames rather than yours. Three consequences: console.error
in the handler prints a copy whose stack points into the runtime,
error instanceof TypeError is false for anything that was not a TypeError
to begin with, and error.cause is where the value you actually threw is. Throw
Error objects; a throw { code: 500 } costs you the stack.
The adapter #
createBugReport is the same state machine as the
React hook, exposed as accessors — message(), elements(), statusMessage()
— over a single signal, so state() in JSX tracks everything and nothing has
to subscribe by hand. Two things are specific to it. It calls onCleanup(destroy)
itself, so call it inside a component: outside an owner it returns a destroy
you are expected to call, and the createSignal warns about the missing owner.
And nothing browser-only runs when you create it — the machine builds its state
without touching window, document or navigator — so a SolidStart or
renderToString pass that constructs it will not fail. The panel and the
screenshot do need the browser, which is what mountBugbottle and
onMount are for.