Error Handling
This guide covers error handling patterns in AIDK—from simple recovery to sophisticated retry strategies.
The onError Hook
Components can handle errors via the onError lifecycle hook:
tsx
class ResilientAgent extends Component {
async onError(error, com) {
console.error("Error occurred:", error);
// Return instructions for the engine
return {
retry: false, // Don't retry the failed operation
continue: true, // Continue execution despite error
};
}
render() {
return (
<>
<Model model={openai("gpt-5.2")} />
<System>You are a helpful assistant.</System>
</>
);
}
}Error Response Options
The onError hook can return different instructions:
typescript
interface ErrorResponse {
// Retry the failed operation
retry?: boolean;
// Continue execution after error (don't throw)
continue?: boolean;
// Custom error to throw instead
throw?: Error;
// Delay before retry (ms)
retryDelay?: number;
// Maximum retry attempts
maxRetries?: number;
}Common Patterns
Pattern 1: Automatic Retry
Retry transient failures with exponential backoff:
tsx
class RetryingAgent extends Component {
private retryCount = signal(0);
private maxRetries = 3;
private lastError = comState<string | null>("lastError", null);
async onError(error, com) {
const count = this.retryCount();
// Only retry certain errors
if (this.isRetryable(error) && count < this.maxRetries) {
this.retryCount.set(count + 1);
return {
retry: true,
retryDelay: Math.pow(2, count) * 1000, // Exponential backoff
};
}
// Don't retry, but continue execution
this.lastError.set(error.message);
return { continue: true };
}
private isRetryable(error: Error): boolean {
const retryableCodes = ["RATE_LIMIT", "TIMEOUT", "SERVICE_UNAVAILABLE"];
return retryableCodes.includes(error.code);
}
render(com) {
const lastError = this.lastError();
return (
<>
<Model model={openai("gpt-5.2")} />
{lastError && (
<System>Note: A previous error occurred: {lastError}</System>
)}
<System>You are a helpful assistant.</System>
</>
);
}
}Pattern 2: Fallback Model
Switch to a backup model on failure:
tsx
class FallbackAgent extends Component {
private useFallback = signal(false);
private primaryErrors = signal(0);
async onError(error, com) {
if (error.source === "model") {
const errors = this.primaryErrors() + 1;
this.primaryErrors.set(errors);
if (errors >= 2) {
this.useFallback.set(true);
}
return { retry: true };
}
return { continue: false }; // Propagate other errors
}
render() {
const model = this.useFallback()
? anthropic("claude-3-haiku") // Fallback
: openai("gpt-5.2"); // Primary
return (
<>
<Model model={model} />
<System>You are a helpful assistant.</System>
</>
);
}
}Pattern 3: Graceful Degradation
Continue with reduced functionality on error:
tsx
class DegradingAgent extends Component {
private features = signal({
search: true,
analysis: true,
generation: true,
});
private degraded = comState<boolean>("degraded", false);
async onError(error, com) {
// Disable the feature that failed
if (error.source === "tool" && error.toolName) {
const features = { ...this.features() };
switch (error.toolName) {
case "web_search":
features.search = false;
break;
case "analyze":
features.analysis = false;
break;
}
this.features.set(features);
this.degraded.set(true);
}
return { continue: true };
}
render(com) {
const features = this.features();
const isDegraded = this.degraded();
return (
<>
<Model model={openai("gpt-5.2")} />
{degraded && (
<System priority="high">
Some features are temporarily unavailable.
</System>
)}
<System>You are a helpful assistant.</System>
{/* Only include working tools */}
{features.search && <SearchTool />}
{features.analysis && <AnalysisTool />}
{features.generation && <GenerationTool />}
</>
);
}
}Pattern 4: Error Logging and Monitoring
Log errors for debugging and monitoring:
tsx
class MonitoredAgent extends Component {
async onError(error, com) {
// Log to monitoring service
await this.logError(error, com);
// Decide how to handle based on severity
if (error.severity === "critical") {
// Alert and stop
await this.alert(error);
return { continue: false };
}
if (error.severity === "warning") {
// Log and continue
return { continue: true };
}
// Retry transient errors
return { retry: true, retryDelay: 1000 };
}
private async logError(error: Error) {
const ctx = context();
await fetch("/api/errors", {
method: "POST",
body: JSON.stringify({
error: error.message,
stack: error.stack,
context: {
userId: ctx.user?.id,
sessionId: ctx.metadata?.sessionId,
},
}),
});
}
private async alert(error: Error) {
await fetch("/api/alerts", {
method: "POST",
body: JSON.stringify({ error: error.message }),
});
}
}Pattern 5: User-Facing Error Messages
Translate technical errors to user-friendly messages:
tsx
class UserFriendlyAgent extends Component {
private userError = signal<string | null>(null);
async onError(error, com) {
const userMessage = this.translateError(error);
this.userError.set(userMessage);
return { continue: true };
}
private translateError(error: Error): string {
const translations: Record<string, string> = {
RATE_LIMIT: "I'm receiving too many requests. Please wait a moment.",
TIMEOUT: "The request took too long. Let me try again.",
INVALID_API_KEY: "There's a configuration issue. Please contact support.",
CONTEXT_LENGTH: "The conversation is too long. Let me summarize.",
};
return translations[error.code] || "Something went wrong. Please try again.";
}
render() {
const error = this.userError();
return (
<>
<Model model={openai("gpt-5.2")} />
{error && (
<AssistantMessage>{error}</AssistantMessage>
)}
<System>You are a helpful assistant.</System>
</>
);
}
}Pattern 6: Circuit Breaker
Stop retrying after repeated failures:
tsx
class CircuitBreakerAgent extends Component {
private failures = signal(0);
private circuitOpen = signal(false);
private lastFailure = signal<number>(0);
private threshold = 5;
private resetTimeout = 30000; // 30 seconds
async onError(error, com) {
const now = Date.now();
const failures = this.failures() + 1;
this.failures.set(failures);
this.lastFailure.set(now);
if (failures >= this.threshold) {
this.circuitOpen.set(true);
}
return { continue: true };
}
async onTickStart(com) {
// Check if circuit should reset
if (this.circuitOpen()) {
const elapsed = Date.now() - this.lastFailure();
if (elapsed > this.resetTimeout) {
// Reset circuit
this.circuitOpen.set(false);
this.failures.set(0);
}
}
}
render() {
if (this.circuitOpen()) {
return (
<AssistantMessage>
The service is temporarily unavailable. Please try again later.
</AssistantMessage>
);
}
return (
<>
<Model model={openai("gpt-5.2")} />
<System>You are a helpful assistant.</System>
</>
);
}
}Tool Error Handling
Tools can handle their own errors:
tsx
const ResilientTool = createTool({
name: "resilient_api",
input: z.object({ query: z.string() }),
handler: async ({ query }) => {
try {
return await apiCall(query);
} catch (error) {
// Return error as result instead of throwing
return {
success: false,
error: error.message,
suggestion: "Try a different query",
};
}
},
});Or throw structured errors:
tsx
import { ToolError } from "aidk";
const ValidatingTool = createTool({
name: "validated_action",
input: z.object({ value: z.number() }),
handler: async ({ value }) => {
if (value < 0) {
throw new ToolError({
code: "VALIDATION_ERROR",
message: "Value must be positive",
retryable: false,
userMessage: "Please provide a positive number",
});
}
return performAction(value);
},
});Error Types
AIDK provides structured error types:
typescript
import {
NotFoundError,
ValidationError,
AbortError,
TimeoutError,
RateLimitError,
} from "aidk-shared";
// Usage
throw new NotFoundError("model", modelId);
throw ValidationError.required("messages");
throw AbortError.timeout(30000);
throw new RateLimitError("Too many requests", { retryAfter: 60 });State Recovery
For long-running agents, persist state for crash recovery:
tsx
class PersistentAgent extends Component {
private customState = comState<any>("customState", null);
async onMount(com) {
const ctx = context();
// Restore state on startup
const savedState = await this.loadState(ctx.metadata?.sessionId);
if (savedState) {
this.customState.set(savedState.customState);
}
}
async onTickEnd(com, state) {
const ctx = context();
// Persist state after each tick
await this.saveState(ctx.metadata?.sessionId, {
timeline: state.current?.timeline,
customState: this.customState(),
});
}
async onError(error, com) {
const ctx = context();
// Save error state for debugging
await this.saveErrorState(ctx.metadata?.sessionId, error);
return { continue: true };
}
private async loadState(sessionId: string) {
return await redis.get(`agent:${sessionId}:state`);
}
private async saveState(sessionId: string, state: any) {
await redis.set(`agent:${sessionId}:state`, state);
}
}Best Practices
- Be specific about retries: Only retry transient errors
- Use exponential backoff: Avoid overwhelming failing services
- Set retry limits: Prevent infinite retry loops
- Degrade gracefully: Continue with reduced functionality when possible
- Log comprehensively: Capture context for debugging
- Translate for users: Don't expose technical errors to end users
- Persist for recovery: Save state for crash recovery in critical flows
Key Takeaways
- onError hook intercepts all errors in the component tree
- Return
{ retry: true }to retry,{ continue: true }to proceed - Use signals to track error state and modify behavior
- Implement fallbacks for critical functionality
- Log errors for monitoring and debugging
Next Steps
- Testing - Test error handling paths
- Tick Lifecycle - Full hook reference
- State Management - Persisting error state