Error Boundaries
When something inside a Whisq component tree throws — a render-time error, a signal-driven recomputation, an event handler, an effect, or even a child component’s setup — you usually want to show fallback UI instead of crashing the whole page. Whisq ships a first-class primitive for exactly this: errorBoundary().
The errorBoundary() primitive
Section titled “The errorBoundary() primitive”Wrap any subtree in errorBoundary(fallback, child):
import { errorBoundary, div, p, button } from "@whisq/core";
errorBoundary( (error, retry) => div({ class: "error-boundary" }, p(`Something went wrong: ${error.message}`), button({ onclick: retry }, "Try again"), ), () => RiskyComponent({}),);fallback—(error, retry) => Node. Called when the wrapped subtree throws. Receives the caught error and aretryfunction that re-runs the child.child—() => Node. The protected subtree. The thunk letserrorBoundaryre-invoke it on retry.
Why use the primitive instead of a hand-rolled try/catch in a component? A try/catch inside a component setup function only sees errors thrown synchronously while that setup runs. Anything that happens later — a child component’s setup, a re-render after a signal changes, an event handler, an async callback — runs outside that try block and is not caught. errorBoundary() is integrated with Whisq’s render machinery and is scoped to the wrapped subtree, so it sees errors thrown anywhere in that subtree’s rendering. Use the primitive — don’t roll your own.
For asynchronous failures (network, timers, deferred promises), see Async data below — resource() is the right tool, not errorBoundary().
Granular boundaries
Section titled “Granular boundaries”Wrap individual sections rather than the entire app — if one widget fails, the rest of the page keeps working:
import { component, errorBoundary, div, h1, p } from "@whisq/core";
const Dashboard = component(() => div( h1("Dashboard"), errorBoundary( (err) => p("Stats unavailable"), () => StatsWidget({}), ), errorBoundary( (err) => p("Chart failed to load"), () => ChartWidget({}), ), errorBoundary( (err) => p("Table unavailable"), () => TableWidget({}), ), ));If the chart throws, the stats and table still render normally.
Error reporting
Section titled “Error reporting”Silently swallowing errors hides bugs. Always log them — either in the fallback callback or via a small reporter helper:
import { errorBoundary, div, p, button } from "@whisq/core";
const reportError = (error: Error, where: string) => { console.error(`[${where}]`, error); // fetch("/api/errors", { method: "POST", body: JSON.stringify({ where, message: error.message }) });};
errorBoundary( (error, retry) => { reportError(error, "ChartWidget"); return div({ class: "error" }, p("This section failed to load."), button({ onclick: retry }, "Retry"), ); }, () => ChartWidget({}),);Async data
Section titled “Async data”For network and other async failures, you don’t need a separate error boundary at all. resource() already exposes loading and error state as signals:
import { resource, div, p, when, button } from "@whisq/core";
const data = resource(() => fetch("/api/data").then((r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.text(); }),);
div( when(() => data.loading(), () => p("Loading…")), when(() => !!data.error(), () => div( p(() => `Error: ${data.error()!.message}`), button({ onclick: () => data.refetch() }, "Retry"), ), ), when(() => !!data.data(), () => p(() => data.data()!)),);Reach for errorBoundary() around a subtree that uses resource() only when you also want to catch unexpected render-time errors in that subtree — resource() handles the documented async failure path, errorBoundary() is the safety net for everything else.
Best practices
Section titled “Best practices”- Wrap at section boundaries, not every component. Logical units (widgets, panels, routes) are the right granularity.
- Always provide actionable fallback UI — explain what failed and offer a retry button when it makes sense.
- Always log or report —
console.errorat minimum; a real reporting service in production. - Use
resource()for async failures; reserveerrorBoundary()for unexpected throws.
Next steps
Section titled “Next steps”- Components — Error Boundaries — short reference for the primitive next to component composition basics.
- Data Fetching —
resource()with built-in error handling. - Performance — Optimizing component rendering.
- Testing — Testing error states.