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#

FailureHandled by
A component throws while building its markupboundary
A binding throws while re-renderingalready 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().