Error Handling & Debugging
Catch render errors with onError, surface async failures through asyncAction, and inspect live apps with the DevTools extension.
Layers of failure
Mates separates render failures (onError), async failures (asyncAction.error), and infrastructure failures (WebSocket status, storage quota). Handle each where the UI can recover — don't wrap every click in try/catch.
Render errors: onError
Register onError in a component's outer function. When that component's setup or template throws during render, every handler receives the error — the component itself renders nothing in production, or a dev error message in development:
import { html, onError } from "mates";
export const Dashboard = () => {
onError((error) => {
reportToSentry(error);
});
return () => html`<div>${riskyRender()}</div>`;
};
Use it per component for local fallback UI, or in a top-level wrapper as an app-wide reporter. Full lifecycle story: onError / onPaint.
Async errors: asyncAction
Don't try/catch around fetches in the template — asyncAction models the failure state so templates can react to it:
import { asyncAction, html } from "mates";
import { Get } from "mates";
const loadPosts = asyncAction(async () => {
return Get("/api/posts");
});
const Posts = () => {
loadPosts();
return () => html`
${loadPosts.loading()
? html`<span class="spinner"></span>`
: loadPosts.error()
? html`<p class="error">Couldn't load posts.
<button @click=${() => loadPosts()}>Retry</button></p>`
: html`${loadPosts.data().map((p) => html`<article>${p.title}</article>`)}`}
`;
};
.error()holds the thrown value — truthy check in the template, no state flags to manage- Re-invoking the action clears the error and re-enters loading
- Interceptors on FetchClient can centralize auth failures and retry policies
Cancellable work: ZeroPromise
For work you need to cancel or re-run — search-as-you-type, polling replacements — ZeroPromise wraps a promise with cancel/restart semantics built on AbortController:
import { ZeroPromise } from "mates";
const query = new ZeroPromise<string>();
fetch(url, { signal: query.signal })
.then((res) => query.resolve(res.statusText))
.catch(() => query.reject(new Error("network error")));
query.cancel(); // aborts the in-flight request
Connection and storage failures
ws()exposes a reactivestatusatom — render connection banners from state, not event spaghetti; reconnects (with exponential backoff) are automatic — WebSocketlsAtom/ssAtomsync to Web Storage — private-mode quota errors surface on write; keep persisted payloads small — StorageonErroralso catches throws from lifecycle hooks registered in the same component
Debugging with DevTools
- Call
renderMatesDevTools()from mates-devtools beforerenderAppto inspect the live component tree and atom state - Core hooks stay no-ops until the panel installs them — omit the package in production builds
- Development builds render a visible error message on a failed component — fix or wrap with
onErrorbefore shipping - Atom subscriptions are tracked per template: if a view isn't updating, check that the atom is read inside the render function — Mental Model