Guides
Error Handling Patterns

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:

  1. Error Classification: Categorizing errors to determine appropriate handling strategies
  2. Retry Policies: Configurable retry behavior for transient failures
  3. Error Propagation: Control over how errors flow through the system
  4. Compensation Logic: Undoing effects of partially completed workflows
  5. Error Boundaries: Containing errors within specific workflow segments

Error Classification

ZoopFlow classifies errors into several categories:

Error TypeDescriptionDefault Handling
ValidationErrorInput/output schema validation failuresNon-retryable, immediate failure
TransientErrorTemporary failures that may resolve with retryAutomatic retry with backoff
PermanentErrorNon-recoverable failuresNo retry, immediate failure
BusinessErrorExpected business logic failuresHandled as normal flow branching
SystemErrorUnderlying platform or infrastructure issuesAutomatic 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

  1. Classify Errors: Use appropriate error types to distinguish between different kinds of failures

  2. Set Proper Retry Policies: Configure retry behavior based on the nature of the operation

  3. Use Checkpoints: Create checkpoints before and after critical operations

  4. Implement Compensation Logic: Always provide compensation actions for operations that need to be reversed

  5. Isolate Error Handling: Use dedicated error handlers to separate business logic from error handling

  6. Log Consistently: Ensure errors are properly logged with context for debugging

  7. Provide Meaningful Error Messages: Include actionable information in error messages

  8. Monitor and Alert: Set up monitoring for error trends and alert on unexpected failures

  9. Test Error Scenarios: Write tests specifically for error scenarios to ensure proper handling

  10. Document Error Responses: Clearly document expected error responses for each workflow ENDOFFILE < /dev/null