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.
| Situation | Default (streaming-safe) | Strict mode |
|---|---|---|
| Unknown identifier | Evaluates to null | console.warn |
| Non-callable callee | Returns null | console.warn |
| Unknown component | Renders a Skeleton | Warns & still renders the skeleton |
| Rejected inline style | Dropped silently | console.warn |
| Missing i18n key | Returns the key | console.warn |
| Trailing object that matches no parameter | Treated as a positional arg | Warns 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.
$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
- Wrap risky subtrees in the
ErrorBoundarycomponent to show a fallback instead of a blank region. - Listen for the
errorevent at the host and render your own toast / banner via the app state or the$toastmanager. - Keep
stricton in staging so warnings show up in your test and QA consoles before they reach production.
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", ...]