Error Handling
EventFlow provides patterns for handling errors gracefully in your state machines.
Error Events
The primary pattern is using explicit error events:
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
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:
machine: @order
// Normal states
#pending
#processing
#completed
// Error states
#payment_failed
#validation_error
#system_errorError states can have their own transitions:
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
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
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
on :submit from @user (api)
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:
machine: @order_saga
on :checkout from @customer (api)
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:
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 |HTTP Error Responses
For API events, return structured error responses to callers using reply:
on :checkout from @customer (api)
? cart is empty
reply 400 bad request with:
| error | "CART_EMPTY" |
| message | "Cart cannot be empty" |
? total is 0
reply 400 bad request with:
| error | "INVALID_TOTAL" |
| message | "Order total must be greater than 0" |
// All validations passed
$order_id becomes uuid()
order moves to #processing
reply 201 created with:
| id | $order_id |
| status | current_state |Named Error Responses
For consistent error formatting across handlers:
machine: @order
response: validation_error
| field | from |
| error | $error_code |
| message | $error_message |
on :checkout from @customer (api)
? cart is empty
$error_code becomes "CART_EMPTY"
$error_message becomes "Cart cannot be empty"
reply 400 with validation_error
on :add_item from @customer (api)
? item is invalid
$error_code becomes "INVALID_ITEM"
$error_message becomes "Item not found"
reply 400 with validation_errorError Response vs Error Event
| Approach | Use When |
|---|---|
reply 4xx with: | API caller needs immediate error response |
emit :error to @actor | Another machine needs to react to the error |
on :checkout from @customer (api)
? cart is empty
// Return HTTP error to API caller
reply 400 bad request with:
| error | "CART_EMPTY" |
// Also notify internal systems if needed
emit :checkout_validation_failed to @analyticsSee Machine Responses for complete error response patterns.
Data Validation Errors
EventFlow provides built-in data validation that automatically emits error events when validation fails.
Event Validation Failure
When event data validation fails, :validation_failed is automatically emitted:
emit :checkout to @order with:
| field | value | validation |
| email | $email | required, string, valid email |
| total | $total | required, number, greater than 0 |
on :validation_failed
emit :checkout_error to @customer with:
| field | value | validation |
| errors | $errors | required |The error payload includes:
event- The event that failed validationsource- The senderfailed_rules- Array of failed validations with field, rule, value, messagesummary- Human-readable summary
Context Constraint Violation
When a context constraint is violated, :constraint_violated is emitted and the action is rolled back:
$retry_count increases by 1
| $retry_count | required, integer, between 0 and 5 |
on :constraint_violated
? $field is "$retry_count"
emit :max_retries_exceeded to @customer
order moves to #failed
?
log constraint violation with $field, $rule, $old_value, $new_valueThe constraint violated payload includes:
field- The variable that violated the constraintold_value- The value before the attempted changenew_value- The value that was attemptedrule- The rule that was violatedmessage- Error message
See Data Validation for complete validation documentation.
Manual Validation Errors
For additional validation logic, catch validation issues early with guards:
on :checkout from @customer (api)
? 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:
machine: @order
on :checkout from @customer (api)
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:
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:
scenario: handle payment failure
given:
cart has items
payment will fail
on :checkout from @customer (api)
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 1scenario: 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
// Good - explicit error events
emit :payment_failed to @order
with $error_code, $error_message
// Avoid - silent failures
? payment failed
// do nothingUse Error States
// Good - clear error states
#payment_failed
#validation_error
#timeout
// Avoid - generic error state
#errorLog Error Context
on :operation_failed from @system
log error with:
| aggregate_id | $id |
| state | current state |
| error | $error |
| timestamp | now |Allow Recovery
// 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