Error Handling
EventFlow provides patterns for handling errors gracefully in your state machines.
Error Events
The primary pattern is using explicit error events:
flow
machine: @payment
on :process_payment from @order
validate card
? card is valid
charge card
? charge successful
emit :payment_success to @order
otherwise
emit :payment_failed to @order
with:
| error_code | "CHARGE_FAILED" |
| message | $error_message |
otherwise
emit :invalid_card to @orderHandling Errors
flow
machine: @order
on :payment_success from @payment
order moves to #paid
on :payment_failed from @payment
$error_code becomes $error_code
$retry_count increases by 1
? $retry_count < 3
order moves to #retrying_payment
emit :payment_request to @payment
otherwise
order moves to #payment_failed
emit :order_failed to @customer
on :invalid_card from @payment
order moves to #needs_payment_update
emit :update_payment_method to @customerError States
Use specific states for error conditions:
flow
machine: @order
// Normal states
#pending
#processing
#completed
// Error states
#payment_failed
#validation_error
#system_errorError states can have their own transitions:
flow
on :retry from @customer
? order is in #payment_failed
order moves to #pending
emit :payment_request to @payment
on :cancel from @customer
? order is in #payment_failed
order moves to #cancelled
emit :refund_if_needed to @paymentRetry Patterns
Simple Retry
flow
on :process from @system
attempt operation
? operation failed
? $attempts < 3
$attempts increases by 1
emit :process to @self // Retry
otherwise
emit :operation_failed to @error_handlerRetry with Backoff
flow
on :process from @system
attempt operation
? operation failed
? $attempts < 5
$attempts increases by 1
$delay becomes $attempts * 2 // Exponential backoff
schedule :retry_process for $delay seconds
otherwise
emit :operation_failed to @error_handlerRetry with State
flow
on> :submit from @user
order moves to #processing
emit :process_order to @processor
on :processing_failed from @processor
? $retry_count < 3
$retry_count increases by 1
order moves to #retrying
emit :process_order to @processor
otherwise
order moves to #failed
emit :submission_failed to @userCompensation (Saga Pattern)
When a multi-step process fails, undo previous steps:
flow
machine: @order_saga
on> :checkout from @customer
emit :reserve_inventory to @inventory
saga moves to #reserving
on :inventory_reserved from @inventory
$inventory_reserved becomes true
emit :charge_payment to @payment
saga moves to #charging
on :payment_charged from @payment
emit :confirm_order to @order
emit :order_confirmed to @customer
saga moves to #completed
// Compensation handlers
on :payment_failed from @payment
? $inventory_reserved
emit :release_inventory to @inventory // Undo inventory
emit :checkout_failed to @customer
saga moves to #failed
on :inventory_unavailable from @inventory
emit :checkout_failed to @customer
saga moves to #failedDead Letter Handling
For events that repeatedly fail:
flow
machine: @error_handler
on :dead_letter from @system
log error details
$failed_events adds $event
emit :alert to @operations
with:
| event_type | $event.type |
| error | $event.error |
| attempts | $event.attempts |Validation Errors
Catch validation issues early:
flow
on> :checkout from @customer
? cart is empty
emit :validation_error to @customer
with "Cart cannot be empty"
? total is 0
emit :validation_error to @customer
with "Order total must be greater than 0"
? shipping address is missing
emit :validation_error to @customer
with "Shipping address is required"
// All validations passed
order moves to #processing
emit :process_order to @processorTimeout Handling
Handle operations that take too long:
flow
machine: @order
on> :checkout from @customer
emit :payment_request to @payment
$payment_requested_at becomes now
order moves to #awaiting_payment
on :payment_success from @payment
order moves to #paid
// Scheduled timeout check
on :check_payment_timeout
triggered: every 5 minutes
for each order in #awaiting_payment
? $payment_requested_at older than 15 minutes
emit :payment_timeout to @order
on :payment_timeout
? order is in #awaiting_payment
? $timeout_count < 2
$timeout_count increases by 1
emit :payment_request to @payment
$payment_requested_at becomes now
otherwise
order moves to #payment_timeout
emit :checkout_failed to @customerError Reporting
Emit error details for monitoring:
flow
on :operation_failed from @processor
emit :error_report to @monitoring
with:
| machine | "@order" |
| aggregate_id | $order_id |
| error_type | $error_type |
| error_msg | $error_message |
| occurred_at | now |
| context | $current_context |Testing Error Paths
Test your error handling:
flow
scenario: handle payment failure
given:
cart has items
payment will fail
on> :checkout from @customer
order moves to #awaiting_payment
emit :payment_request to @payment
on :payment_failed from @payment
$retry_count increases by 1
order moves to #payment_failed
expect:
= order is in #payment_failed
= $retry_count equals 1flow
scenario: retry exhaustion
given:
cart has items
$retry_count is 2
payment will fail
on :payment_failed from @payment
$retry_count increases by 1
? $retry_count >= 3
order moves to #permanently_failed
emit :order_cancelled to @customer
expect:
= order is in #permanently_failed
= @customer received :order_cancelledBest Practices
Be Explicit About Errors
flow
// Good - explicit error events
emit :payment_failed to @order
with $error_code, $error_message
// Avoid - silent failures
? payment failed
// do nothingUse Error States
flow
// Good - clear error states
#payment_failed
#validation_error
#timeout
// Avoid - generic error state
#errorLog Error Context
flow
on :operation_failed from @system
log error with:
| aggregate_id | $id |
| state | current state |
| error | $error |
| timestamp | now |Allow Recovery
flow
// Always provide a path out of error states
on :retry from @admin
? order is in #failed
order moves to #pending
emit :reprocess to @processor
on :cancel from @admin
? order is in #failed
order moves to #cancelled
emit :notify_customer to @notification