Loop Engineering Error Handling
Error handling strategies for autonomous AI agent loops — classification, recovery patterns, graceful degradation, and fault tolerance in loop engineering.
Loop Engineering Error Handling
In traditional software, error handling is a defensive measure. In loop engineering, it is the core mechanism that makes autonomous coding possible. When an AI agent operates in a closed loop, errors are not exceptional — they are expected. The system must detect, classify, recover from, and learn from errors on every iteration.
This guide covers the complete error handling lifecycle for agentic loop systems, from classification taxonomies to recovery patterns and observability strategies.
Why Error Handling Is Critical for Autonomous Loops
Unlike traditional programs that crash on errors, an autonomous loop must:
- Continue operating through transient failures without human intervention
- Distinguish between recoverable and unrecoverable errors
- Prevent error cascades where one failure triggers a chain of downstream failures
- Accumulate learning from errors across iterations to avoid repeating them
- Know when to stop rather than burning resources on unsolvable problems
┌─────────────────────────────────────────────────────────┐
│ Error Handling in a Loop Context │
│ │
│ Traditional: Code ──▶ Error ──▶ Crash ──▶ Human Fix │
│ │
│ Loop Engineering: │
│ Code ──▶ Error ──▶ Classify ──▶ Recover ──▶ Continue │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ Unrecover │──▶ Escalate to Human │
│ │ able? │──▶ Graceful Degradation │
│ └──────────┘ │
└─────────────────────────────────────────────────────────┘
Error Classification Taxonomy
The first step in effective error handling is classification. Different error types require fundamentally different recovery strategies.
Error Categories
| Category | Example | Recoverable? | Recovery Strategy |
|---|---|---|---|
| Transient | Rate limit (429), network timeout | Yes | Retry with backoff |
| Permanent | Syntax error in generated code | Yes | Auto-correct loop |
| Cascading | Context overflow from accumulated errors | Partial | Context reduction |
| Context | Token limit exceeded, context window full | Yes | Summarize or truncate |
| Permission | File access denied, read-only filesystem | No | Escalate to human |
| Semantic | Code compiles but behavior is wrong | Yes | Test-driven correction |
| Resource | Out of memory, disk full | Partial | Reduce scope, clean up |
Transient Errors
Transient errors are temporary and self-resolving. They occur when external dependencies are momentarily unavailable.
// Common transient errors in agentic loops
interface TransientError {
type: 'rate_limit' | 'timeout' | 'network' | 'service_unavailable';
retryable: true;
suggestedDelay: number;
}
function classifyTransientError(error: unknown): TransientError | null {
const msg = String(error);
if (msg.includes('429') || msg.includes('rate limit')) {
return { type: 'rate_limit', retryable: true, suggestedDelay: 60000 };
}
if (msg.includes('ETIMEDOUT') || msg.includes('timeout')) {
return { type: 'timeout', retryable: true, suggestedDelay: 5000 };
}
if (msg.includes('ECONNRESET') || msg.includes('network')) {
return { type: 'network', retryable: true, suggestedDelay: 3000 };
}
return null;
}
Permanent Errors
Permanent errors indicate a fundamental problem that won't self-resolve. They require the loop to take corrective action.
// Permanent errors require the loop to fix something
interface PermanentError {
type: 'syntax_error' | 'test_failure' | 'missing_dependency' | 'type_error';
context: string; // The code or config that caused the error
fixStrategy: string; // Hint for the LLM on how to fix
}
function classifyPermanentError(error: unknown): PermanentError | null {
const msg = String(error);
if (msg.includes('SyntaxError') || msg.includes('Unexpected token')) {
return {
type: 'syntax_error',
context: extractRelevantCode(error),
fixStrategy: 'Fix the syntax error in the generated code'
};
}
if (msg.includes('Test failed') || msg.includes('AssertionError')) {
return {
type: 'test_failure',
context: extractTestOutput(error),
fixStrategy: 'Fix the failing test or the code under test'
};
}
return null;
}
Cascading Errors
Cascading errors occur when one failure triggers a chain reaction. These are particularly dangerous in loop engineering because they can cause the loop to spiral.
Example cascade in an agentic loop:
LLM generates code with a typo
│
▼
Compilation fails with cryptic error
│
▼
Error message is fed back to LLM as context
│
▼
Context grows large, LLM loses focus
│
▼
LLM generates more errors, not fixing the original
│
▼
Context overflows token limit
│
▼
Loop crashes or enters unrecoverable state
Error Detection Strategies
Before you can handle errors, you must detect them. Agentic loops interact with tools that report errors in many different formats.
Tool Output Parsing
interface ToolResult {
exitCode: number;
stdout: string;
stderr: string;
filesChanged: string[];
}
function detectErrors(result: ToolResult): LoopError[] {
const errors: LoopError[] = [];
// Exit code analysis
if (result.exitCode !== 0) {
errors.push({
severity: 'error',
source: 'exit_code',
message: `Tool exited with code ${result.exitCode}`,
detail: result.stderr || result.stdout
});
}
// Stderr analysis (some tools write warnings to stderr)
const stderrLines = result.stderr.split('\n');
for (const line of stderrLines) {
if (line.includes('error') || line.includes('Error')) {
errors.push({
severity: 'error',
source: 'stderr',
message: line.trim(),
detail: result.stderr
});
}
}
// Test output parsing
const testMatch = result.stdout.match(
/(\d+) passing.*?(\d+) failing/m
);
if (testMatch && parseInt(testMatch[2]) > 0) {
errors.push({
severity: 'test_failure',
source: 'test_runner',
message: `${testMatch[2]} tests failing`,
detail: result.stdout
});
}
return errors;
}
Timeout Detection
async function withTimeout<T>(
promise: Promise<T>,
ms: number,
label: string
): Promise<T> {
return Promise.race([
promise,
new Promise<never>((_, reject) =>
setTimeout(
() => reject(new Error(`Timeout: ${label} exceeded ${ms}ms`)),
ms
)
)
]);
}
LLM Output Validation
function validateLLMOutput(output: string): ValidationResult {
const issues: string[] = [];
// Check for empty output
if (!output.trim()) {
issues.push('LLM returned empty output');
}
// Check for hallucinated tool calls
if (output.includes('```bash') && output.includes('rm -rf /')) {
issues.push('Potentially destructive command detected');
}
// Check for incomplete code blocks
const openBlocks = (output.match(/```/g) || []).length;
if (openBlocks % 2 !== 0) {
issues.push('Unclosed code block in output');
}
return {
valid: issues.length === 0,
issues
};
}
Error Recovery Patterns
Retry with Escalation
When simple retries are not enough, escalate the recovery approach with each attempt.
async function retryWithEscalation<T>(
fn: () => Promise<T>,
options: {
maxAttempts: number;
escalationStrategies: (() => void)[];
}
): Promise<T> {
for (let attempt = 0; attempt < options.maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
// Apply escalation strategy for next attempt
if (attempt < options.escalationStrategies.length) {
console.log(`Escalating: applying strategy ${attempt + 1}`);
options.escalationStrategies[attempt]();
}
await sleep(1000 * Math.pow(2, attempt));
}
}
throw new Error('All retry attempts exhausted');
}
// Usage: escalate from simple retry to context reduction
await retryWithEscalation(
() => llm.generateCode(task),
{
maxAttempts: 5,
escalationStrategies: [
() => { /* Strategy 1: Add error context */ },
() => { /* Strategy 2: Reduce context window */ },
() => { /* Strategy 3: Switch to smaller model */ },
() => { /* Strategy 4: Break task into sub-tasks */ },
]
}
);
Context Reduction
When the context window overflows, strategically trim accumulated context while preserving the most relevant information.
function reduceContext(context: LoopContext): LoopContext {
const totalTokens = estimateTokens(context);
if (totalTokens < context.maxTokens * 0.8) {
return context; // No reduction needed
}
const reduced = { ...context };
// Priority 1: Summarize old iterations
if (context.iterationHistory.length > 5) {
const recent = context.iterationHistory.slice(-3);
const oldSummary = summarizeIterations(
context.iterationHistory.slice(0, -3)
);
reduced.iterationHistory = [
{ summary: oldSummary },
...recent
];
}
// Priority 2: Trim file contents to relevant sections
reduced.files = reduced.files.map(file => ({
...file,
content: extractRelevantSections(file.content, context.task)
}));
// Priority 3: Remove resolved error messages
reduced.errors = reduced.errors.filter(
e => !e.resolved
);
return reduced;
}
Model Fallback
When the primary model fails, fall back to a simpler, faster, or more reliable model.
interface ModelConfig {
name: string;
maxTokens: number;
costPerToken: number;
reliability: number; // 0-1
}
async function generateWithFallback(
prompt: string,
models: ModelConfig[]
): Promise<string> {
for (const model of models) {
try {
return await llm.complete(prompt, {
model: model.name,
maxTokens: model.maxTokens
});
} catch (error) {
console.warn(
`Model ${model.name} failed: ${error}. Trying next model.`
);
}
}
throw new Error('All models failed');
}
// Use expensive model first, fall back to cheaper ones
const models = [
{ name: 'claude-opus-4', maxTokens: 200000, costPerToken: 0.015, reliability: 0.99 },
{ name: 'claude-sonnet-4', maxTokens: 200000, costPerToken: 0.003, reliability: 0.97 },
{ name: 'claude-haiku-3.5', maxTokens: 200000, costPerToken: 0.001, reliability: 0.95 }
];
Human Escalation (HITL)
When all automated recovery fails, escalate to a human operator with a clear summary of what went wrong.
interface EscalationRequest {
loopId: string;
iteration: number;
task: string;
errors: LoopError[];
attemptedFixes: string[];
currentState: string;
options: string[]; // Suggested actions for human
}
function escalateToHuman(request: EscalationRequest): void {
const summary = [
`Loop ${request.loopId} requires human intervention`,
`Task: ${request.task}`,
`Iteration: ${request.iteration}`,
`Errors:`,
...request.errors.map(e => ` - ${e.message}`),
`Attempted fixes:`,
...request.attemptedFixes.map(f => ` - ${f}`),
`Suggested actions:`,
...request.options.map(o => ` [ ] ${o}`)
].join('\n');
console.error(summary);
// In production: send to alerting system, pause loop,
// create ticket, notify on-call engineer
}
Graceful Degradation
When a loop cannot complete its primary objective, graceful degradation ensures it still delivers partial value.
interface DegradationLevel {
level: number;
description: string;
capabilities: string[];
}
const degradationLevels: DegradationLevel[] = [
{
level: 0,
description: 'Full capability',
capabilities: ['implement', 'test', 'fix', 'optimize']
},
{
level: 1,
description: 'Reduced scope',
capabilities: ['implement', 'test', 'fix']
},
{
level: 2,
description: 'Core only',
capabilities: ['implement', 'fix']
},
{
level: 3,
description: 'Minimal viable',
capabilities: ['implement']
},
{
level: 4,
description: 'Manual intervention needed',
capabilities: []
}
];
function determineDegradationLevel(
errorCount: number,
iterationCount: number,
resourceUsage: number
): number {
// Calculate degradation based on multiple signals
const errorRatio = errorCount / iterationCount;
if (errorRatio > 0.8 || resourceUsage > 0.9) return 4;
if (errorRatio > 0.6 || resourceUsage > 0.7) return 3;
if (errorRatio > 0.4) return 2;
if (errorRatio > 0.2) return 1;
return 0;
}
Dead Loop Prevention
The most critical safety mechanism in any agentic loop is preventing infinite execution. Multiple independent safeguards ensure the loop always terminates.
class LoopGuard {
private iterationCount = 0;
private startTime = Date.now();
private consecutiveErrors = 0;
private lastProgressIteration = 0;
private errorHistory: string[] = [];
constructor(
private maxIterations: number = 50,
private maxTimeMs: number = 600_000,
private maxConsecutiveErrors: number = 5
) {}
check(): { safe: boolean; reason?: string } {
// Guard 1: Maximum iterations
if (this.iterationCount >= this.maxIterations) {
return { safe: false, reason: 'Max iterations reached' };
}
// Guard 2: Time budget
if (Date.now() - this.startTime > this.maxTimeMs) {
return { safe: false, reason: 'Time budget exceeded' };
}
// Guard 3: Consecutive error streak
if (this.consecutiveErrors >= this.maxConsecutiveErrors) {
return { safe: false, reason: 'Too many consecutive errors' };
}
// Guard 4: No progress detection
if (this.iterationCount - this.lastProgressIteration > 10) {
return { safe: false, reason: 'No progress detected in 10 iterations' };
}
return { safe: true };
}
recordIteration(hasProgress: boolean, error?: string): void {
this.iterationCount++;
if (hasProgress) {
this.lastProgressIteration = this.iterationCount;
this.consecutiveErrors = 0;
}
if (error) {
this.consecutiveErrors++;
this.errorHistory.push(error);
}
}
}
Error Logging and Observability
For production loop engineering systems, structured error logging enables debugging, analysis, and improvement over time.
interface LoopEvent {
timestamp: string;
loopId: string;
iteration: number;
phase: 'reason' | 'act' | 'verify' | 'fix';
type: 'success' | 'error' | 'warning' | 'info';
message: string;
metadata?: Record<string, unknown>;
}
class LoopLogger {
private events: LoopEvent[] = [];
log(event: Omit<LoopEvent, 'timestamp'>): void {
this.events.push({ ...event, timestamp: new Date().toISOString() });
}
getErrorSummary(): string {
const errors = this.events.filter(e => e.type === 'error');
const errorTypes = new Map<string, number>();
for (const error of errors) {
const key = error.message.split(':')[0];
errorTypes.set(key, (errorTypes.get(key) || 0) + 1);
}
return Array.from(errorTypes.entries())
.sort((a, b) => b[1] - a[1])
.map(([type, count]) => ` ${type}: ${count}x`)
.join('\n');
}
// Export for analysis
exportJSON(): string {
return JSON.stringify(this.events, null, 2);
}
}
Complete Error Handling Flow
The following ASCII diagram shows how all these components work together in a production loop:
┌────────────────────────────────────────────────────────────┐
│ LOOP ITERATION │
│ │
│ ┌──────────┐ │
│ │ REASON │◀────────────────────────────────┐ │
│ └────┬─────┘ │ │
│ │ │ │
│ ▼ │ │
│ ┌──────────┐ ┌─────────────────────┐ │ │
│ │ ACT │────▶│ ERROR DETECTION │ │ │
│ └──────────┘ └─────────┬───────────┘ │ │
│ │ │ │
│ ┌─────────┼─────────┐ │ │
│ ▼ ▼ ▼ │ │
│ ┌──────────┐┌────────┐┌────────┐ │ │
│ │Transient ││Perm. ││Context │ │ │
│ │ ││ ││ │ │ │
│ └────┬─────┘└───┬────┘└───┬────┘ │ │
│ │ │ │ │ │
│ ▼ ▼ ▼ │ │
│ ┌──────────┐┌────────┐┌────────┐ │ │
│ │ RETRY ││ FIX ││ REDUCE │ │ │
│ │ backoff ││ loop ││context │ │ │
│ └────┬─────┘└───┬────┘└───┬────┘ │ │
│ │ │ │ │ │
│ ▼ ▼ ▼ │ │
│ ┌──────────────────────────┐ │ │
│ │ LOOP GUARD CHECK │ │ │
│ └────────────┬─────────────┘ │ │
│ │ │ │
│ ┌──────┼──────┐ │ │
│ ▼ ▼ ▼ │ │
│ SAFE TIMEOUT STAGNANT │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌────────────────┐ │ │
│ │ │ DEGRADE / │ │ │
│ │ │ ESCALATE │ │ │
│ │ └────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ OBSERVABILITY LAYER │ │
│ │ Log every error, recovery action, and state │ │
│ │ transition for post-mortem analysis │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
Key Takeaways
- Error classification is the foundation — you cannot recover effectively if you don't know what kind of error you are dealing with
- Transient errors (rate limits, timeouts) should be retried with exponential backoff and jitter; permanent errors (syntax, test failures) require the auto-correction loop to fix them
- Context overflow is the most insidious error in loop engineering — it corrupts subsequent iterations and can cascade into complete loop failure
- Multiple independent guards prevent dead loops: max iterations, time budgets, consecutive error limits, and no-progress detection
- Graceful degradation ensures the system delivers partial value even when it cannot achieve the full objective
- Model fallback chains provide resilience when primary LLM APIs are degraded, trading capability for reliability
- Human escalation with structured context is the final safety net — include error history, attempted fixes, and suggested next actions
- Observability is non-negotiable in production loops: structured logging of every error, recovery action, and state transition enables debugging and continuous improvement