Error Handling
Shiro surfaces failures by where they happen. Your application should branch on that, not on a single string message.
Categories#
| Category | Meaning | Typical response |
|---|---|---|
| Provider error | Adapter / model call failed | Retry or degrade |
| Tool error | execute threw or timed out | Fix args / service; retry if safe |
| Approval rejection | Reviewer denied the tool | Tell the user; do not force |
| Validation error | Output schema .parse failed | Repair or ask for clarification |
| Configuration error | Engine / agent misconfigured | Fix startup; do not retry blindly |
| Runtime error | Runner cannot continue safely | Fail the run; keep the trace |
Core exports typed errors such as ProviderError, ToolExecutionError, ValidationError,
ApprovalRejectedError, ConfigurationError, and ShiroError.
Example#
import { ApprovalRejectedError, ShiroError } from "@shiro-sdk/core";
try {
return await engine.execute(agent, input, { sessionId });
} catch (error) {
if (error instanceof ApprovalRejectedError) {
return { ok: false, reason: "rejected_by_reviewer" };
}
if (error instanceof ShiroError) {
// Include error.run-related context / your trace runId in logs
throw error;
}
throw error;
}Always preserve enough identity (runId, session id) to find the trace later.
Why categories matter#
Retries are not universally safe. A provider timeout mid-refund is not permission to call
issueRefund again without an idempotency key. See Reliability.
Best practices#
- Inspect the trace before retrying
- Return stable error codes to clients
- Treat approval rejection as product behavior
- Normalize vendor errors inside providers
Common mistakes#
catch (e) { res.status(500) }for every failure mode- Retrying writes automatically
- Losing traces when a run fails
- Parsing error messages instead of using error types