Error Handling Patterns in ZoopFlow
Error handling is a critical aspect of reliable workflow orchestration. ZoopFlow provides several patterns and mechanisms to handle errors effectively.
Core Error Handling Concepts
ZoopFlow's error handling system is built on these key concepts:
- Error Classification: Categorizing errors to determine appropriate handling strategies
- Retry Policies: Configurable retry behavior for transient failures
- Error Propagation: Control over how errors flow through the system
- Compensation Logic: Undoing effects of partially completed workflows
- Error Boundaries: Containing errors within specific workflow segments
Error Classification
ZoopFlow classifies errors into several categories:
| Error Type | Description | Default Handling |
|---|---|---|
ValidationError | Input/output schema validation failures | Non-retryable, immediate failure |
TransientError | Temporary failures that may resolve with retry | Automatic retry with backoff |
PermanentError | Non-recoverable failures | No retry, immediate failure |
BusinessError | Expected business logic failures | Handled as normal flow branching |
SystemError | Underlying platform or infrastructure issues | Automatic retry with notification |
Creating Custom Error Types
You can define custom error types for your domain:
import { createErrorType } from '@zoopflow/core/error';
// Create a custom error type
export const PaymentDeclinedError = createErrorType<{
orderId: string;
reason: string;
}>('PaymentDeclinedError', {
category: 'BusinessError',
retryable: false,
severity: 'warning'
});
// Using the custom error
throw new PaymentDeclinedError('Payment was declined', {
orderId: 'order-123',
reason: 'insufficient_funds'
});Retry Policies
ZoopFlow provides configurable retry policies for handling transient errors:
import { defineStep } from '@zoopflow/core';
const paymentStep = defineStep({
id: 'payment.process',
version: '1.0.0',
// Retry configuration
retry_config: {
max_attempts: 3,
backoff_coefficient: 1.5,
initial_interval_seconds: 1,
max_interval_seconds: 10,
non_retryable_error_types: [
'ValidationError',
'PaymentDeclinedError'
]
},
execute: async (input, context) => {
// Step implementation
}
});Dynamic Retry Policies
You can also set retry policies dynamically based on the specific error:
import { defineFlow } from '@zoopflow/core';
import { PaymentService } from './services';
const checkoutFlow = defineFlow({
id: 'checkout.process',
version: '1.0.0',
steps: async (input, context) => {
try {
// Flow implementation
} catch (error) {
// Dynamic retry based on error
if (error.code === 'RATE_LIMITED') {
return context.retry({
max_attempts: 5,
backoff_coefficient: 2,
initial_interval_seconds: 10
});
}
throw error;
}
}
});Error Handling Patterns
1. Try-Catch Pattern
The basic try-catch pattern allows you to handle errors within a workflow:
import { defineFlow } from '@zoopflow/core';
const checkoutFlow = defineFlow({
id: 'checkout.process',
version: '1.0.0',
steps: async (input, context) => {
try {
const paymentResult = await context.executeStep(processPayment, input);
const inventoryResult = await context.executeStep(updateInventory, input);
const emailResult = await context.executeStep(sendConfirmation, {
orderId: input.orderId,
paymentId: paymentResult.id
});
return { success: true };
} catch (error) {
context.logger.error('Checkout failed', { error });
// Handle specific error types
if (error instanceof PaymentDeclinedError) {
await context.executeStep(notifyCustomerOfFailure, {
orderId: input.orderId,
reason: error.metadata.reason
});
return { success: false, reason: 'payment_declined' };
}
// Re-throw unknown errors
throw error;
}
}
});2. Error Handler Pattern
You can define a dedicated error handler for a flow:
import { defineFlow } from '@zoopflow/core';
const checkoutFlow = defineFlow({
id: 'checkout.process',
version: '1.0.0',
steps: async (input, context) => {
// Flow implementation without try-catch
const paymentResult = await context.executeStep(processPayment, input);
return { success: true };
},
// Dedicated error handler
errorHandler: async (error, context, input) => {
context.logger.error('Checkout error', { error });
// Handle different error types
if (error instanceof PaymentDeclinedError) {
await context.executeStep(notifyCustomerOfFailure, {
orderId: input.orderId,
reason: error.metadata.reason
});
return { success: false, reason: 'payment_declined' };
}
// Default error response
return { success: false, reason: 'unknown_error' };
}
});3. Saga Pattern (Compensation)
The saga pattern handles failures by executing compensating actions:
import { defineFlow } from '@zoopflow/core';
const checkoutFlow = defineFlow({
id: 'checkout.process',
version: '1.0.0',
steps: async (input, context) => {
let paymentResult;
let inventoryResult;
try {
// Step 1: Process payment
paymentResult = await context.executeStep(processPayment, input);
context.createCheckpoint({ paymentCompleted: true, paymentId: paymentResult.id });
// Step 2: Update inventory
inventoryResult = await context.executeStep(updateInventory, {
orderId: input.orderId,
items: input.items
});
context.createCheckpoint({ inventoryUpdated: true });
// Step 3: Send confirmation
await context.executeStep(sendConfirmation, {
orderId: input.orderId
});
return { success: true };
} catch (error) {
// Compensating actions in reverse order
if (inventoryResult) {
// Undo inventory update
await context.executeStep(restoreInventory, {
orderId: input.orderId,
items: input.items
});
}
if (paymentResult) {
// Refund payment
await context.executeStep(refundPayment, {
paymentId: paymentResult.id
});
}
// Notify customer of failure
await context.executeStep(notifyCustomerOfFailure, {
orderId: input.orderId
});
return { success: false, reason: error.message };
}
}
});4. Circuit Breaker Pattern
The circuit breaker pattern prevents repeated failures:
import { defineFlow, CircuitBreaker } from '@zoopflow/core';
// Create a circuit breaker
const paymentCircuitBreaker = new CircuitBreaker({
serviceName: 'payment-service',
failureThreshold: 5,
resetTimeout: 60000 // 1 minute
});
const checkoutFlow = defineFlow({
id: 'checkout.process',
version: '1.0.0',
steps: async (input, context) => {
try {
// Use circuit breaker
const paymentResult = await paymentCircuitBreaker.execute(() =>
context.executeStep(processPayment, input)
);
return { success: true };
} catch (error) {
if (error.code === 'CIRCUIT_OPEN') {
// Circuit is open, use fallback
return { success: false, reason: 'service_unavailable' };
}
throw error;
}
}
});5. Retry with Delay Pattern
The retry with delay pattern handles rate limiting:
import { defineFlow, sleep } from '@zoopflow/core';
const checkoutFlow = defineFlow({
id: 'checkout.process',
version: '1.0.0',
steps: async (input, context) => {
let attempts = 0;
const maxAttempts = 3;
while (attempts < maxAttempts) {
try {
return await context.executeStep(processPayment, input);
} catch (error) {
attempts++;
if (error.code === 'RATE_LIMITED' && attempts < maxAttempts) {
// Calculate exponential backoff
const delayMs = Math.pow(2, attempts) * 1000;
context.logger.info(`Rate limited, retrying in ${delayMs}ms`);
await sleep(delayMs);
continue;
}
throw error;
}
}
}
});Best Practices
-
Classify Errors: Use appropriate error types to distinguish between different kinds of failures
-
Set Proper Retry Policies: Configure retry behavior based on the nature of the operation
-
Use Checkpoints: Create checkpoints before and after critical operations
-
Implement Compensation Logic: Always provide compensation actions for operations that need to be reversed
-
Isolate Error Handling: Use dedicated error handlers to separate business logic from error handling
-
Log Consistently: Ensure errors are properly logged with context for debugging
-
Provide Meaningful Error Messages: Include actionable information in error messages
-
Monitor and Alert: Set up monitoring for error trends and alert on unexpected failures
-
Test Error Scenarios: Write tests specifically for error scenarios to ensure proper handling
-
Document Error Responses: Clearly document expected error responses for each workflow ENDOFFILE < /dev/null