Error Model
Noetic uses structured NoeticError objects with error codes and recovery hints.
Quick Example
import { AgentHarness, isNoeticError } from '@noetic-tools/core';
declare const harness: AgentHarness;
try {
const result = await harness.execute('Do something');
} catch (e) {
if (isNoeticError(e)) {
console.log(e.noeticError.kind); // e.g. 'step_failed'
console.log(e.message); // Human-readable description
}
}Noetic wraps every framework-level failure in a structured NoeticError discriminated union. Instead of catching generic exceptions and guessing what went wrong, you can match on the kind field and access typed metadata for each error variant.
NoeticError Type
NoeticError is a discriminated union with a kind field. Each variant carries context-specific data:
kind | Key Fields | When it occurs |
|---|---|---|
step_failed | stepId, cause: Error, retriesExhausted | A step throws after exhausting retries. |
llm_refused | stepId, refusal: string | The model returns a refusal instead of content. |
llm_parse_error | stepId, raw: string, schema: ZodType, zodError: ZodError | Structured output fails Zod validation. |
llm_rate_limit | stepId, retryAfter?: number | The LLM provider returns a rate-limit response. |
fork_partial | stepId, succeeded[], failed[] | Thrown by fork mode 'all' when any path fails, and by 'race' when every path fails. (Mode 'settle' never throws this — it reports failures in its SettleResult array instead.) |
channel_timeout | channelName, timeout: number | A recv() call — or a back-pressured send() parked on a full queue — exceeded its timeout. |
channel_closed | channelName | Attempted to send on a closed external channel. |
cancelled | reason?: string | The execution was cancelled via ctx.abort() or harness.cancel(). Pending channel recv calls (and parked back-pressure send calls) on the aborted context reject with this kind immediately. |
budget_exceeded | field: 'cost' | 'steps' | 'duration', limit, actual | A budget guard detected the run exceeded its limits. |
steering_denied | guidance?: string | A memory layer's steering hook (beforeToolCall / afterModelCall) returned a deny decision and the runtime blocked the action. guidance carries the corrective feedback, if any. |
handle_evicted | handleId, stepId, gracePeriodMs | DetachedHandle.await() found the subprocess adapter persistently returning null for a previously spawned handle (evicted by a restart without restore, or a storage inconsistency). Raised after a short grace period instead of polling forever. |
item_schema_mismatch | category: 'items' | 'developerMessages' | 'toolCalls' | 'toolResults', itemType?: string | An item failed extension-schema validation in the ItemSchemaRegistry: a category item (e.g. a tool result) matched none of the schemas registered for its category, or an unknown item type matched no registered schema under strict mode. itemType is set for the unknown-type case. |
Full Type Definition
type NoeticError =
| { kind: 'step_failed'; stepId: string; cause: Error; retriesExhausted: boolean }
| { kind: 'llm_refused'; stepId: string; refusal: string }
| { kind: 'llm_parse_error'; stepId: string; raw: string; schema: ZodType; zodError: ZodError }
| { kind: 'llm_rate_limit'; stepId: string; retryAfter?: number }
| { kind: 'fork_partial'; stepId: string; succeeded: Array<{ stepId: string; value: unknown }>; failed: Array<{ stepId: string; error: NoeticError }> }
| { kind: 'channel_timeout'; channelName: string; timeout: number }
| { kind: 'channel_closed'; channelName: string }
| { kind: 'cancelled'; reason?: string }
| { kind: 'budget_exceeded'; field: 'cost' | 'steps' | 'duration'; limit: number; actual: number }
| { kind: 'steering_denied'; guidance?: string }
| { kind: 'handle_evicted'; handleId: string; stepId: string; gracePeriodMs: number }
| { kind: 'item_schema_mismatch'; category: 'items' | 'developerMessages' | 'toolCalls' | 'toolResults'; itemType?: string };NoeticErrorImpl Class
NoeticErrorImpl extends the standard Error class, so it works with try/catch and stack traces. The structured error data lives on the .noeticError property:
class NoeticErrorImpl extends Error {
readonly noeticError: NoeticError;
constructor(error: NoeticError);
}The message string is auto-generated from the error kind and its fields. For example, a step_failed error produces:
Step 'fetch-data' failed: Connection refusedisNoeticError Type Guard
Use isNoeticError() to narrow an unknown catch value:
import { isNoeticError } from '@noetic-tools/core';
function isNoeticError(e: unknown): e is NoeticErrorImpl;This is an instanceof check against NoeticErrorImpl with a structural fallback: any Error carrying a noeticError object whose kind is a string also passes. The fallback keeps the guard reliable when more than one copy of the framework is loaded (mixed src/dist resolution, duplicated node_modules), where a plain instanceof would fail across module copies. The inner kind is not validated against the closed union, so errors minted by a newer framework version still pass.
Handling Errors in Loops
Loops accept an onError callback that receives the structured error and returns a recovery action:
import { step, loop, until } from '@noetic-tools/core';
const resilientLoop = loop({
id: 'retry-loop',
steps: [step.llm({ id: 'generate', model: 'openai/gpt-4o' })],
until: until.noToolCalls(),
onError(error, ctx) {
if (error.kind === 'llm_rate_limit') {
return 'retry'; // Try the same iteration again
}
if (error.kind === 'step_failed' && !error.retriesExhausted) {
return 'skip'; // Move to the next iteration
}
return 'abort'; // Re-throw the error
},
});| Return value | Behavior |
|---|---|
'retry' | Re-execute the same iteration immediately. |
'skip' | Skip this iteration and continue with the previous output. |
'abort' | Re-throw the error, ending the loop. |
Matching on Error Kind
Because NoeticError is a discriminated union, TypeScript narrows the type after checking kind:
declare const e: unknown;
if (isNoeticError(e)) {
const err = e.noeticError;
switch (err.kind) {
case 'llm_parse_error':
// TypeScript knows: err.raw, err.schema, err.zodError
console.log('Validation errors:', err.zodError.issues);
break;
case 'fork_partial':
// TypeScript knows: err.succeeded, err.failed
console.log(`${err.succeeded.length} paths ok, ${err.failed.length} failed`);
break;
case 'budget_exceeded':
// TypeScript knows: err.field, err.limit, err.actual
console.log(`${err.field} limit: ${err.limit}, used: ${err.actual}`);
break;
}
}Related Pages
- Context & Event Log --
ctx.abort()produces acancellederror. - Channels --
channel_timeoutandchannel_closederrors. - Loop & Until -- the
onErrorrecovery callback. - AgentHarness --
harness.cancel()triggers cancellation errors.