Error Boundaries
Catch errors from a subtree's mount and reactive updates, render a fallback, and retry.
Things break, and when they do you don't want one error taking down the whole app. ImplementBoundary isolates failures. When something in its subtree throws, the boundary swaps in its Catch branch instead of letting the error propagate.
import { ImplementBoundary, ImplementEffect } from "@implementjs/core";
ImplementBoundary(IssuePage(id)).Catch((error, reset) =>
Div(P(error.message), Button({ onClick: reset }, "Retry")),
);
reset remounts the original children from scratch. Without a Catch, an error renders nothing in place of the subtree.
What a boundary catches
- Errors thrown synchronously while the subtree mounts, including a component function throwing while building its tree.
- Errors raised during reactive updates inside the structural helpers.
If,ForEach,Switch,Key,Await, andPortalall route their signal-driven re-syncs to the nearest boundary. AForEachkey error or a render function throwing on update lands here. - Errors thrown in
Lifecycle.onMountand inImplementEffecteffects.
Thrown values that aren't Errors are normalized into one.
What it deliberately does not catch
- Event handler errors. Wrap handler bodies in
try/catchyourself. - Promise rejections. Those belong to
Await.Catch. - A derived getter that throws when called outside any reactive sync (a plain
.get()in your own code) propagates to the caller as usual.
Behavior details
- Boundaries nest. An error goes to the nearest one at or above where it was raised. If the
Catchbranch itself throws, the error escalates to the next boundary up rather than looping. With no boundary above, it rethrows. - The swap to the
Catchbranch is deferred a microtask (still before paint), so an error raised mid-mount never re-enters a mount pass already on the stack. - Boundaries follow the logical tree, so a failing subtree inside a
Portalstill reaches the boundary that wraps the portal.
Placement
Put boundaries where a failure has a sensible replacement UI. Around a route's page, a widget, a row. Not one giant boundary at the root:
const router = Router({
"/issues": {
layout: (child) => Shell(ImplementBoundary(child).Catch(PageError)),
"/": () => Issues(),
"/:id": { "/": ({ id }) => Issue(id) },
},
});
One more composition tool to go. Sometimes the right DOM parent for a component isn't where it lives in your tree, and for that there's Portal.