Guide

Error handling & debugging.

Aktion fails safely by design — partial, mid-stream LLM output should never crash the page. That same design hides bugs from a human developer. This guide explains the failure modes, how to surface them, and how to read each kind of error.

The mental model in one line

By default Aktion swallows problems so a half-streamed program still renders. Flip on strict while developing to turn that silence into console.warns, listen for the host error event to route failures into telemetry, and wrap risky subtrees in ErrorBoundary so a throw shows a card instead of a blank page.

Two audiences, two modes

The runtime serves a streaming LLM (which emits incomplete programs token-by-token) and a human developer (who wants loud feedback). The default is the safe, silent behaviour for streaming; strict mode flips on developer-grade diagnostics.

SituationDefault (streaming-safe)Strict mode
Unknown identifierEvaluates to nullconsole.warn
Non-callable calleeReturns nullconsole.warn
Unknown componentRenders a SkeletonWarns & still renders the skeleton
Rejected inline styleDropped silentlyconsole.warn
Missing i18n keyReturns the keyconsole.warn
Trailing object that matches no parameterTreated as a positional argWarns about the likely named→positional flip

The demo below shows the default safe behaviour in action: a reference to a component that doesn’t exist renders a Skeleton placeholder instead of crashing — exactly what you want while an LLM is still streaming the function that defines it.

Live — an unknown component falls back to a Skeleton
$app(Column([
  Text("Known components render normally:", { variant: "large-heavy" }),
  Badge("I exist", "success"),
  Text("An unknown component becomes a Skeleton (no crash):"),
  ChartThatDoesNotExist(),
], { gap: "md" }))

Enabling strict mode

Add the strict attribute to the host element. Use it in development and tests; leave it off in the live LLM surface where partial output is expected.

<aktion-app strict></aktion-app>

Strict mode only changes diagnostics — it never changes rendered output. A program behaves identically with and without it, so you can safely toggle it per environment.

Parse errors

Parse errors carry a line, column, and a message, and are shown in an error banner on the element. While streaming, parse errors are tolerated (the tail may simply be incomplete); once a full document is set, they surface through the error event.

The error event

The host dispatches a composed, bubbling error CustomEvent. Listen for it to route problems into your own logging / telemetry:

const app = document.querySelector("aktion-app");
app.addEventListener("error", (e) => {
  for (const { line, column, message } of e.detail.errors) {
    console.error(`[aktion] ${line}:${column} ${message}`);
    // forward to Sentry / your telemetry here
  }
});

The same event shape ({ errors: [{ line, column, message }] }) is used for both parse failures and aborted renders — safety-budget trips report { line: 0, column: 0, message }.

The runtime guards

Render-loop guard

If a $name = … write happens during render, the runtime applies it without scheduling another render (otherwise the render would re-trigger itself forever) and logs a warning. Seeing this warning means you are writing reactive state in render position — move the write into an event handler or effect, or seed it with a PascalCase component / $state. See Reactivity & rendering.

Safety-budget guard

Each render runs under a budget (component depth 150, 250 000 iterations, 100 000 array entries). When a limit trips the render is aborted, an error event fires, and the previous tick’s DOM is kept so the user still sees something. A budget trip almost always means accidental recursion or an unbounded loop — check for a component that renders itself, or a range/repeat with a runaway size. See Performance.

Diagnose with DevTools

The DevTools panel gives you a state inspector, a commit profiler, an effect timeline, and a per-render “why did this render” reason. When something re-renders unexpectedly or a value looks wrong, start there: inspect the live atom values, then read the commit reason to trace the trigger.

Surfacing errors to end users

ErrorBoundary & $util.onError

ErrorBoundary(child, { fallback?, onError?, showDetails? }) catches a render error in its subtree and shows a fallback instead of breaking the page. Omit fallback for a built-in friendly card (with a Retry button); pass showDetails: true in development to include the message.

ErrorBoundary(
  RiskyWidget(),
  { fallback: Callout("This widget is unavailable.", { tone: "danger" }), showDetails: true }
)

For action failures (a throw inside an onClick or effect), register a program‑level sink with $util.onError(fn). It runs fn({ error, source }) before the default logging — ideal for a toast or analytics report:

$util.onError(({ error, source }) => {
  $toast.error("Something went wrong")
  reportToAnalytics(source, String(error))
})

Typo suggestions with suggestComponent

When a program references a component that doesn’t exist, the runtime can suggest the closest matches. The tooling export suggestComponent(name, library) returns “did you mean” candidates — useful in a build/lint step or a custom editor.

import { suggestComponent } from "aktion-runtime";

suggestComponent("Buttn");   // → ["Button", ...]

Next