Skip to content

Error Handling

typescript
import { SchemaViolationError, MaxRetriesExceededError } from "@aviasole/shapecraft";

try {
  const result = await generate(model, schema, prompt, { maxRetries: 3 });
} catch (err) {
  if (err instanceof MaxRetriesExceededError) {
    console.error(`Failed after ${err.attempts} attempts`);
  }
  if (err instanceof SchemaViolationError) {
    console.error("Raw output:", err.raw);
    console.error("Errors:", err.validationErrors);
  }
}

Every error type

All of these are exported from the root entrypoint.

ErrorThrown whenCarries
SchemaViolationErrorone attempt's output failed validation (structural, semantic, or minConfidence)raw, validationErrors
MaxRetriesExceededErrorgenerate() exhausted maxRetries without a valid resultattempts
TimeoutErrorone attempt exceeded timeoutMs, or signal firedtimeoutMs
MaxTurnsExceededErrorturnaround or runAgents() hit its maxTurns guardturns
SkillExecutionErrora skill's own handler threwskill, cause
MaxSkillTurnsExceededErrorrunSkillLoop() hit maxTurns with no terminal skillturns, memory
MaxToolTurnsExceededErrorgenerateWithTools() hit maxTurns still requesting toolsturns
ToolExecutionErrora tool's own handler threwtoolName, cause

MaxTurnsExceededError is also re-exported from @aviasole/shapecraft/agentic, so catching it around runAgents() doesn't force a second import.

What retries and what doesn't

Only a SchemaViolationError is retried. That's the deliberate line: a failed structural check is something re-prompting the model can plausibly fix, so generate() retries it up to maxRetries before giving up with MaxRetriesExceededError.

Everything else propagates immediately:

  • TimeoutError - a hung or slow backend retrying just as slowly rarely self-heals.
  • SkillExecutionError / ToolExecutionError - the dispatch was structurally valid; your handler's own logic failed. Re-prompting the model can't fix a broken handler.
  • The Max*TurnsExceededError family - a loop guard tripping is a budget decision for the caller, not something to retry blindly.

MaxSkillTurnsExceededError carries the loop's JSON-serializable memory, so it's resumable rather than fatal - persist it and call runSkillLoop() again with { memory: err.memory } and a fresh budget:

typescript
import { runSkillLoop, MaxSkillTurnsExceededError } from "@aviasole/shapecraft";

try {
  const { result } = await runSkillLoop(model, registry, "Refund order #4521");
} catch (err) {
  if (err instanceof MaxSkillTurnsExceededError) {
    const { result } = await runSkillLoop(model, registry, "Continue.", { memory: err.memory });
  }
}

Released under the Apache-2.0 License.