Skip to Content
Error recovery

Error recovery

A model sometimes emits a broken expression. Containment keeps the failure inside its element. Recovery swaps in a corrected element by re-emitting its key (partial replacement), which clears the error and retries the render. The three ways to recover differ in who notices the failure and who asks for the fix.

Containment: the error slot

Every element renders inside its own error boundary. If its props, hidden, loading, each or seed throws, or its implementation does, only that element is replaced by the error slot.

A corrected re-emit retries a failed seed, filling only paths still unset (first-writer-wins). A seed that succeeded never re-runs.

Classification: EntryError and onError

Re-emitting helps only when the model made the mistake, not when your server returned a 500. So every failure is an EntryError from @uicast/core, classified by whose code raised it:

class EntryError extends Error { reason: EntryErrorReason; // the specific verdict — see the table fault: "document" | "environment" | "unknown"; // derived from reason elementKey?: string; // the element the failure belongs to cause?: unknown; // the original error static is(err: unknown): err is EntryError; // cross-copy-safe check — use over `instanceof` }
reasonMeaningfault
expression-syntaxthe expression doesn’t parsedocument
guardrail-violationthe expression uses blocked syntax or APIs, or a set is not scopes.<scope>.<field>document
budget-exceededthe expression ran past its step, time or allocation budgetdocument
unknown-referencean unregistered function, global, or scope pathdocument
invalid-entrya field has the wrong type, like a seed that is not an arraydocument
unknown-componentthe component name has no implementationdocument
invalid-listeach didn’t evaluate to an array, or as names a scope that already existsdocument
invalid-propsthe evaluated props fail the def’s schema, before render runsdocument
invalid-argumentsa host function rejected the document’s argumentsdocument
host-functiona host function threw or rejectedenvironment
host-initthe host init callback threw or rejectedenvironment
implementationthe render threw on props the def’s schema acceptsenvironment
expression-runtimea valid expression threw at runtime (a read through null)unknown
unknownescaped from an uninstrumented pathunknown

fault is the verdict: document means the model wrote the bug and a re-emission can fix it; environment means host code failed; unknown could be either.

The same object reaches the error slot and the provider’s onError, called once per failure, including callback failures, which render no slot:

<RendererProvider evaluator={evaluator} implementations={implementations} onError={(error) => { if (error.fault === "document") { // the model can fix this — feed it back (see self-healing below) } else { reportToMonitoring(error); } }} > <EntriesRenderer entries={entries} /> </RendererProvider>

1. Self-correction

The model re-emits a broken element later in the same output:

{"key":"card","component":"Card","props":{"expr":"({ title: window.title })"},"children":["kpi"]} {"key":"kpi","component":"Stat","props":{"literal":{"label":"Products","value":128}}} {"key":"card","component":"Card","props":{"literal":{"title":"Inventory"}},"children":["kpi"]}

Line 1 fails with unknown-reference. Line 3 fixes it: same key, and children still names kpi, so that child survives untouched.

The prompt tells the model to correct a mistake once, not to retry repeatedly. A replacement line also works in a later turn.

2. User-triggered recovery

The model never sees run-time failures, such as an expression that throws on real data or a host function that rejects. Here the host’s error slot offers a Recover button that sends the failure back as a new turn, formatted by getErrorRecoveryPrompt:

import { getErrorRecoveryPrompt } from "@uicast/core/prompt"; getErrorRecoveryPrompt({ failures: [ { key: "chart", message: "chartData.map is not a function", reason: error.reason, // optional }, ], });

returns:

Rendered UI has errors: - Entry `chart`: chartData.map is not a function (expression-runtime — valid expression threw at runtime) Re-emit each failed entry fixed, same `key`. Fix cause: when it comes from another entry (like `seed` elsewhere), fix that entry too.

reason adds the failure class to the line. An environment fault gets none, as it is not the model’s to fix.

The Recover button is host UI, since only the host knows how to reach its model:

import type { ErrorComponentProps } from "@uicast/react"; const fallbackComponents = { error: ({ error }: ErrorComponentProps) => { const key = error.elementKey; return ( <div role="alert"> {error.reason === "host-function" ? "Couldn't reach the server." : error.message} {error.fault !== "environment" && key && ( <button onClick={() => sendRecoveryTurn({ key, message: error.message, reason: error.reason, }) } > Recover </button> )} </div> ); }, };

sendRecoveryTurn is host code: on a page it starts an edit run, in a chat it calls sendMessage. Offer Recover for every fault but environment; an unknown one is worth a single attempt.

3. Self-healing

With no person involved, onError sends each "document" fault as the same recovery turn:

<RendererProvider evaluator={evaluator} implementations={implementations} onError={(error) => { if (error.fault !== "document" || !error.elementKey) { reportToMonitoring(error); return; } sendRecoveryTurn({ key: error.elementKey, message: error.message, reason: error.reason, }); }} > <EntriesRenderer entries={entries} /> </RendererProvider>

The gate is stricter than the button’s: an automatic loop sends back only what is provably the model’s.

The error slot stays until the corrected line lands.

Budget the retries. Cap attempts per elementKey at one or two. Past the cap, leave the Recover button in place, so self-healing falls back to the user instead of looping.

Last updated on