- 
          
- 
                Notifications
    You must be signed in to change notification settings 
- Fork 0
Error Handling
        Jean-Marc Strauven edited this page Aug 6, 2025 
        ·
        1 revision
      
    β Step Groups | Home | Conditions & Branching β
Laravel Flowpipe provides comprehensive error handling strategies to make your workflows resilient and production-ready.
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
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|exponentialProvide 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\FetchUserFromCacheStepRoll 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\UpdateInventoryStepTemporarily 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 secondssteps:
  - 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 failssteps:
  - 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
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()
            ])
        ];
    }
}<?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: 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 stepssteps:
  - 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\FulfillOrderStepIsolate 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: continueDistributed 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\RevertCrmUpdateStepflow: 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 secondsflow: 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<?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);
    }
}<?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']);
    }
}# 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 enrichmentpublic 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);
}steps:
  - type: action
    name: critical-operation
    class: CriticalOperationStep
    on_error: stop
    error_hooks:
      - type: action
        class: AlertOpsTeamStep
      - type: action
        class: CreateIncidentStep# Provide degraded functionality instead of complete failure
- type: action
  name: get-recommendations
  class: GetRecommendationsStep
  fallback:
    type: action
    class: GetPopularItemsStep  # Fallback to popular items- Conditions & Branching - Dynamic workflow execution
- Queue Integration - Asynchronous error handling
- PHP Steps - Creating resilient step classes
- Example User Registration - See error handling in practice
Laravel Flowpipe - YAML-driven workflow engine for Laravel
GitHub: Laravel Flowpipe Repository | Support: GitHub Issues
Quick Navigation: Home β’ Installation β’ Configuration β’ Commands β’ Examples
π§ Developed by Grazulex