Error boundaries — boundary
Sometimes a piece of your UI fails to build — a third-party widget throws, or a component chokes on malformed data. By default that error propagates and nothing renders: one bad subtree blanks the whole page. boundary contains the failure to that subtree and shows a fallback instead.
import { html, boundary } from "./src/index.js";
html`<section>
${boundary(
() => RiskyWidget(), // try to render this
(err) => html`<p class="error">Couldn't load this section.</p>` // show this if it throws
)}
</section>`;If RiskyWidget() throws while building its markup, the rest of the page renders fine and the fallback appears in its place.
What it catches#
boundary catches a synchronous throw during setup/render — the component function itself throwing while it builds its result. That's the one case that would otherwise break mount.
It also cleans up: anything the failed setup created (like an effect) is disposed, so a half-built component can't leave a zombie running.
What it doesn't catch — and what to use instead#
| Failure | Handled by |
|---|---|
| A component throws while building its markup | boundary |
| A binding throws while re-rendering | already contained by the core — that binding is isolated, the rest keeps working |
An await / API call rejects | @zoijs/resource / @zoijs/action — read their error() state |
boundary is deliberately narrow: it's for the synchronous build error, not a catch-all.
The fallback#
fallback is a value, or a function of the error:
boundary(() => Chart(data), (err) => html`<p>Chart failed: ${err.message}</p>`);In development the caught error is also logged to the console; in production it's silent.
It renders once#
boundary isn't reactive and has no retry(). To try again, re-mount the subtree — change its key in an each list, or navigate with the router. Re-mounting runs the component fresh.
When to reach for it#
Wrap subtrees you don't fully control: third-party widgets, user-authored content, or anything built from data you didn't shape. Don't blanket-wrap your whole app — a boundary that hides every error also hides your bugs (which is why it logs in dev).
Next: Lists with each().