MATES / Docs

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 reactive status atom — render connection banners from state, not event spaghetti; reconnects (with exponential backoff) are automatic — WebSocket
  • lsAtom/ssAtom sync to Web Storage — private-mode quota errors surface on write; keep persisted payloads small — Storage
  • onError also catches throws from lifecycle hooks registered in the same component

Debugging with DevTools

  • Call renderMatesDevTools() from mates-devtools before renderApp to 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 onError before 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