Skip to content

Error Handling

Jean-Marc Strauven edited this page Aug 6, 2025 · 1 revision

🚨 Error Handling

← Step Groups | Home | Conditions & Branching β†’

Laravel Flowpipe provides comprehensive error handling strategies to make your workflows resilient and production-ready.

🧠 Core Concept

Error handling in Laravel Flowpipe operates at multiple levels:

  • Step Level: Handle errors in individual steps
  • Flow Level: Handle errors across the entire workflow
  • Group Level: Handle errors within step groups

πŸ›‘οΈ Error Handling Strategies

1. Retry Strategy

Automatically retry failed steps with configurable backoff:

steps:
  - type: action
    name: api-call
    class: App\Flowpipe\Steps\ApiCallStep
    on_error: retry
    retry_attempts: 3
    retry_delay: 1000        # Milliseconds
    retry_backoff: exponential  # linear|exponential

2. Fallback Strategy

Provide alternative logic when a step fails:

steps:
  - type: action
    name: fetch-user-data
    class: App\Flowpipe\Steps\FetchUserFromApiStep
    fallback:
      type: action
      class: App\Flowpipe\Steps\FetchUserFromCacheStep

3. Compensation Strategy

Roll back changes when the flow fails:

flow: payment-processing
description: Process payment with rollback capability

compensation:
  - type: action
    class: App\Flowpipe\Steps\RefundPaymentStep
  - type: action
    class: App\Flowpipe\Steps\RestoreInventoryStep
  - type: action
    class: App\Flowpipe\Steps\NotifyAdminStep

steps:
  - type: action
    class: App\Flowpipe\Steps\ChargePaymentStep
  - type: action
    class: App\Flowpipe\Steps\UpdateInventoryStep

4. Circuit Breaker Pattern

Temporarily disable failing services:

steps:
  - type: action
    name: external-api-call
    class: App\Flowpipe\Steps\ExternalApiStep
    circuit_breaker:
      failure_threshold: 5
      timeout: 60000        # 60 seconds
      half_open_timeout: 30000  # 30 seconds

πŸŽ›οΈ Step-Level Error Handling

Basic Error Handling

steps:
  - type: action
    name: critical-operation
    class: App\Flowpipe\Steps\CriticalOperationStep
    on_error: stop          # stop|continue|retry
    
  - type: action
    name: optional-operation
    class: App\Flowpipe\Steps\OptionalOperationStep
    on_error: continue      # Continue flow even if this fails

Advanced Step Configuration

steps:
  - type: action
    name: payment-processing
    class: App\Flowpipe\Steps\ProcessPaymentStep
    
    # Retry configuration
    on_error: retry
    retry_attempts: 3
    retry_delay: 2000
    retry_backoff: exponential
    retry_multiplier: 2.0
    
    # Fallback if all retries fail
    fallback:
      type: action
      class: App\Flowpipe\Steps\QueuePaymentForLaterStep
      
    # Timeout configuration
    timeout: 30000          # 30 seconds
    
    # Custom error handling
    error_handler: App\Flowpipe\ErrorHandlers\PaymentErrorHandler

πŸ”„ PHP-Based Error Handling

Custom Error Handlers

<?php

namespace App\Flowpipe\ErrorHandlers;

use Grazulex\LaravelFlowpipe\Contracts\ErrorHandler;
use Throwable;

class PaymentErrorHandler implements ErrorHandler
{
    public function handle(Throwable $error, array $payload, array $context): array
    {
        // Log the error
        logger()->error('Payment processing failed', [
            'error' => $error->getMessage(),
            'payload' => $payload,
            'context' => $context
        ]);
        
        // Determine recovery strategy based on error type
        if ($error instanceof PaymentDeclinedException) {
            return [
                'action' => 'fallback',
                'payload' => array_merge($payload, [
                    'payment_status' => 'declined',
                    'retry_suggested' => true
                ])
            ];
        }
        
        if ($error instanceof PaymentGatewayTimeoutException) {
            return [
                'action' => 'retry',
                'delay' => 5000,
                'payload' => $payload
            ];
        }
        
        // Default: stop flow
        return [
            'action' => 'stop',
            'payload' => array_merge($payload, [
                'error' => $error->getMessage()
            ])
        ];
    }
}

Resilient Steps

<?php

namespace App\Flowpipe\Steps;

use Closure;
use Grazulex\LaravelFlowpipe\Contracts\FlowStep;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

class ResilientApiCallStep implements FlowStep
{
    public function handle(mixed $payload, Closure $next): mixed
    {
        $cacheKey = "api_data_{$payload['user_id']}";
        
        try {
            // Try primary API
            $response = Http::timeout(10)->get('https://api.primary.com/users/' . $payload['user_id']);
            
            if ($response->successful()) {
                $data = $response->json();
                Cache::put($cacheKey, $data, 300); // Cache for 5 minutes
                $payload['user_data'] = $data;
                return $next($payload);
            }
            
            throw new \Exception('Primary API failed: ' . $response->status());
            
        } catch (\Exception $e) {
            // Fallback to cached data
            $cachedData = Cache::get($cacheKey);
            if ($cachedData) {
                logger()->warning('Using cached data due to API failure', [
                    'error' => $e->getMessage(),
                    'user_id' => $payload['user_id']
                ]);
                
                $payload['user_data'] = $cachedData;
                $payload['data_source'] = 'cache';
                return $next($payload);
            }
            
            // Fallback to secondary API
            try {
                $response = Http::timeout(15)->get('https://api.backup.com/users/' . $payload['user_id']);
                if ($response->successful()) {
                    $payload['user_data'] = $response->json();
                    $payload['data_source'] = 'backup';
                    return $next($payload);
                }
            } catch (\Exception $backupError) {
                logger()->error('Both primary and backup APIs failed', [
                    'primary_error' => $e->getMessage(),
                    'backup_error' => $backupError->getMessage()
                ]);
            }
            
            throw new \Exception('All data sources failed');
        }
    }
}

🌊 Flow-Level Error Handling

Global Error Handling

flow: critical-business-process
description: Mission-critical workflow with comprehensive error handling

# Global error configuration
on_error: compensate
timeout: 300000             # 5 minutes total

# Compensation steps (run if flow fails)
compensation:
  - type: action
    name: rollback-database-changes
    class: App\Flowpipe\Steps\RollbackDatabaseStep
    
  - type: action
    name: restore-external-state
    class: App\Flowpipe\Steps\RestoreExternalStateStep
    
  - type: action
    name: notify-operations-team
    class: App\Flowpipe\Steps\NotifyOperationsStep
    config:
      urgency: high
      include_payload: false

steps:
  # ... workflow steps

Conditional Error Handling

steps:
  - type: action
    name: process-payment
    class: App\Flowpipe\Steps\ProcessPaymentStep
    
  - condition:
      field: payment_status
      operator: equals
      value: failed
    then:
      # Error recovery steps
      - type: action
        name: queue-for-retry
        class: App\Flowpipe\Steps\QueuePaymentRetryStep
      - type: action
        name: notify-customer
        class: App\Flowpipe\Steps\NotifyPaymentFailureStep
    else:
      # Success path
      - type: action
        name: fulfill-order
        class: App\Flowpipe\Steps\FulfillOrderStep

πŸ”§ Advanced Error Patterns

Bulkhead Pattern

Isolate failures to prevent cascade:

flow: user-onboarding
description: User onboarding with isolated components

steps:
  # Critical path (must succeed)
  - type: group
    name: core-account-setup
    on_error: stop
    
  # Non-critical path (can fail without stopping flow)
  - type: group
    name: optional-integrations
    on_error: continue
    
  - type: group
    name: analytics-tracking
    on_error: continue

Saga Pattern

Distributed transaction management:

flow: distributed-order-processing
description: Order processing across multiple services

steps:
  # Step 1: Reserve inventory
  - type: action
    name: reserve-inventory
    class: App\Flowpipe\Steps\ReserveInventoryStep
    compensation:
      type: action
      class: App\Flowpipe\Steps\ReleaseInventoryStep
      
  # Step 2: Process payment
  - type: action
    name: process-payment
    class: App\Flowpipe\Steps\ProcessPaymentStep
    compensation:
      type: action
      class: App\Flowpipe\Steps\RefundPaymentStep
      
  # Step 3: Update external systems
  - type: action
    name: update-crm
    class: App\Flowpipe\Steps\UpdateCrmStep
    compensation:
      type: action
      class: App\Flowpipe\Steps\RevertCrmUpdateStep

Timeout Management

flow: time-sensitive-process
description: Process with granular timeout control

timeout: 120000             # Global timeout: 2 minutes

steps:
  - type: action
    name: quick-validation
    class: App\Flowpipe\Steps\QuickValidationStep
    timeout: 5000           # 5 seconds
    
  - type: action
    name: external-api-call
    class: App\Flowpipe\Steps\ExternalApiStep
    timeout: 30000          # 30 seconds
    on_timeout: fallback
    fallback:
      type: action
      class: App\Flowpipe\Steps\UseCachedDataStep
      
  - type: action
    name: final-processing
    class: App\Flowpipe\Steps\FinalProcessingStep
    timeout: 60000          # 60 seconds

πŸ“Š Real-World Example: E-commerce Order

flow: resilient-order-processing
description: E-commerce order processing with comprehensive error handling

# Global configuration
timeout: 300000
on_error: compensate

# Compensation strategy
compensation:
  - type: action
    name: release-reserved-inventory
    class: App\Flowpipe\Steps\ReleaseInventoryStep
  - type: action
    name: refund-payment
    class: App\Flowpipe\Steps\RefundPaymentStep
  - type: action
    name: notify-customer-cancellation
    class: App\Flowpipe\Steps\NotifyOrderCancellationStep

send:
  order_id: "ORD-12345"
  items: [...]
  customer: {...}

steps:
  # Step 1: Validate order (critical)
  - type: action
    name: validate-order
    class: App\Flowpipe\Steps\ValidateOrderStep
    timeout: 10000
    on_error: stop
    
  # Step 2: Check inventory (with fallback)
  - type: action
    name: check-inventory
    class: App\Flowpipe\Steps\CheckInventoryStep
    timeout: 15000
    on_error: retry
    retry_attempts: 2
    fallback:
      type: action
      class: App\Flowpipe\Steps\BackorderStep
      
  # Step 3: Process payment (critical with retries)
  - type: action
    name: process-payment
    class: App\Flowpipe\Steps\ProcessPaymentStep
    timeout: 45000
    on_error: retry
    retry_attempts: 3
    retry_backoff: exponential
    
  # Step 4: Fulfillment (can be queued if fails)
  - type: action
    name: create-fulfillment
    class: App\Flowpipe\Steps\CreateFulfillmentStep
    timeout: 30000
    on_error: fallback
    fallback:
      type: action
      class: App\Flowpipe\Steps\QueueFulfillmentStep
      
  # Step 5: Notifications (non-critical)
  - type: action
    name: send-confirmation
    class: App\Flowpipe\Steps\SendConfirmationStep
    timeout: 10000
    on_error: continue
    
  # Step 6: Analytics (non-critical)
  - type: action
    name: track-conversion
    class: App\Flowpipe\Steps\TrackConversionStep
    timeout: 5000
    on_error: continue

πŸ§ͺ Testing Error Scenarios

Unit Testing Error Handling

<?php

namespace Tests\Unit\Flowpipe\Steps;

use Tests\TestCase;
use App\Flowpipe\Steps\ProcessPaymentStep;
use App\Exceptions\PaymentDeclinedException;

class ProcessPaymentStepTest extends TestCase
{
    public function test_handles_payment_decline_gracefully()
    {
        $step = new ProcessPaymentStep();
        $payload = [
            'amount' => 100.00,
            'payment_method' => 'declined_card'
        ];

        $this->expectException(PaymentDeclinedException::class);
        
        $step->handle($payload, fn($data) => $data);
    }
    
    public function test_successful_payment_processing()
    {
        $step = new ProcessPaymentStep();
        $payload = [
            'amount' => 100.00,
            'payment_method' => 'valid_card'
        ];

        $result = $step->handle($payload, fn($data) => $data);

        $this->assertEquals('success', $result['payment_status']);
        $this->assertArrayHasKey('transaction_id', $result);
    }
}

Integration Testing Flows

<?php

namespace Tests\Feature\Flowpipe;

use Tests\TestCase;
use Grazulex\LaravelFlowpipe\Flowpipe;

class ErrorHandlingTest extends TestCase
{
    public function test_flow_handles_step_failure_with_fallback()
    {
        $result = Flowpipe::fromYaml('resilient-order-processing')
            ->send([
                'order_id' => 'TEST-ORDER',
                'simulate_payment_failure' => true
            ])
            ->execute();

        // Should not fail completely due to fallback strategies
        $this->assertTrue($result->isSuccessful());
        $this->assertEquals('queued', $result->data['fulfillment_status']);
    }
    
    public function test_compensation_runs_on_critical_failure()
    {
        $result = Flowpipe::fromYaml('resilient-order-processing')
            ->send([
                'order_id' => 'TEST-ORDER',
                'simulate_critical_failure' => true
            ])
            ->execute();

        $this->assertFalse($result->isSuccessful());
        $this->assertTrue($result->data['compensation_executed']);
        $this->assertTrue($result->data['inventory_released']);
    }
}

πŸ’‘ Best Practices

1. Fail Fast, Recover Smart

# Validate inputs early
- type: action
  name: validate-critical-inputs
  class: ValidateInputsStep
  on_error: stop              # Fail fast on invalid input

# Recover gracefully later
- type: action
  name: optional-enrichment
  class: EnrichDataStep
  on_error: continue          # Continue without enrichment

2. Meaningful Error Messages

public function handle(mixed $payload, Closure $next): mixed
{
    if (!isset($payload['user_id'])) {
        throw new InvalidArgumentException(
            'User ID is required for profile creation. ' .
            'Ensure the previous step provides user_id in the payload.'
        );
    }
    
    return $next($payload);
}

3. Monitoring and Alerting

steps:
  - type: action
    name: critical-operation
    class: CriticalOperationStep
    on_error: stop
    error_hooks:
      - type: action
        class: AlertOpsTeamStep
      - type: action
        class: CreateIncidentStep

4. Graceful Degradation

# Provide degraded functionality instead of complete failure
- type: action
  name: get-recommendations
  class: GetRecommendationsStep
  fallback:
    type: action
    class: GetPopularItemsStep  # Fallback to popular items

🎯 What's Next?

πŸš€ Laravel Flowpipe

🏠 Home

🏁 Getting Started

πŸ“š Core Concepts

πŸš€ Advanced Features

πŸ› οΈ Tools & Configuration

πŸ“– Examples


πŸ”— GitHub Repository

Clone this wiki locally