-
-
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|exponential
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
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
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
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
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
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 steps
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
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
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
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
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
<?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 enrichment
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);
}
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