Skip to content

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 @order

Handling 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 @customer

Error States

Use specific states for error conditions:

flow
machine: @order

// Normal states
#pending
#processing
#completed

// Error states
#payment_failed
#validation_error
#system_error

Error 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 @payment

Retry 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_handler

Retry 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_handler

Retry 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 @user

Compensation (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 #failed

Dead 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 @processor

Timeout 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 @customer

Error 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 1
flow
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_cancelled

Best 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 nothing

Use Error States

flow
// Good - clear error states
#payment_failed
#validation_error
#timeout

// Avoid - generic error state
#error

Log 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

Released under the MIT License.