Skip to content

Interruptions - Faults

CMDx::Fault is the exception execute! raises on failure. Skipped and successful results never raise. A Fault wraps the originating failed Result — the leaf at the bottom of any propagation chain — and delegates task, context, and chain to it. See Exceptions for the full hierarchy.

What's on a Fault

Accessor Returns Notes
fault.result CMDx::Result The failed result that originated the failure (after walking origin)
fault.task Class<CMDx::Task> The failing task class (fault.result.task)
fault.context CMDx::Context The failing task's frozen context
fault.chain CMDx::Chain The chain of every result produced during the run
fault.message String I18nProxy.tr(result.reason) — translates when the reason is an i18n key, otherwise passes through; falls back to the localized cmdx.reasons.unspecified when reason is nil
fault.backtrace Array<String> From result.backtrace or result.cause&.backtrace_locations, cleaned via task.settings.backtrace_cleaner when configured

Use fault.result to read the failed outcome's reason, metadata, cause, origin, state, and status.

Note

Fault wraps failures from fail!, throw!, or errors.add. If Runtime rescued an ordinary StandardError (result.cause is non-Fault), execute! re-raises that original exception instead. In workflows, fault.task always points at the leaf that failed, so Fault.for?(LeafTask) works the same flat or nested.

Fault Handling

begin
  ProcessTicket.execute!(ticket_id: 456)
rescue CMDx::Fault => e
  logger.error "Ticket processing failed: #{e.message}"
  logger.info  "Failing task: #{e.task}"
  notify_admin(e.result.metadata[:error_code])
end

When you need to keep working with the result rather than rescuing, use execute and inspect it directly:

result = ProcessTicket.execute(ticket_id: 456)

result.on(:failed) do |r|
  logger.error "Ticket processing failed: #{r.reason}"
  notify_admin(r.metadata[:error_code], context: r.context)
end

Either form gives you the same data: with execute!, reach for fault.result / fault.context / fault.chain; with execute, work with the returned result directly.

Advanced Matching

Task-Specific Matching

Fault.for?(*task_classes) returns an anonymous matcher subclass suitable for rescue. It matches any fault whose task is (or inherits from) one of the given classes:

begin
  DocumentWorkflow.execute!(document_data: data)
rescue CMDx::Fault.for?(FormatValidator, ContentProcessor) => e
  # Handle only document-related failures
  retry_with_alternate_parser(e.result.metadata)
end

Reason-Specific Matching

Fault.reason?(reason) returns an anonymous matcher subclass that matches any fault whose result.reason equals the given string:

begin
  ProcessPayment.execute!(payment_data: data)
rescue CMDx::Fault.reason?("Payment declined") => e
  notify_customer(e.context.customer_id)
end

Custom Logic Matching

Fault.matches? takes a block returning true/false against the fault. Use it for arbitrary predicates — metadata, status, cause class, etc.:

begin
  ReportGenerator.execute!(report: report_data)
rescue CMDx::Fault.matches? { |f| f.result.metadata[:attempt_count].to_i > 3 } => e
  abandon_report_generation(e)
rescue CMDx::Fault.matches? { |f| f.result.metadata[:error_type] == "memory" } => e
  increase_memory_and_retry(e)
end

Note

Each call to for? / matches? returns a fresh anonymous matcher subclass, so they can be stacked across multiple rescue clauses but cannot be combined into a single matcher.

Fault Propagation

Use throw! to re-raise an upstream failed result through the current task. The propagated signal mirrors the original's state, status, and reason and attaches the current caller_locations as the backtrace. It's a no-op when the argument isn't failed? — skipped or successful results are never converted into failures.

Basic Propagation

class ReportGenerator < CMDx::Task
  def work
    # No-op when the upstream result wasn't failed
    throw!(DataValidator.execute(context))

    # Or guard explicitly
    perms = CheckPermissions.execute(context)
    throw!(perms) if perms.failed?

    generate_report
  end
end

Additional Metadata

Pass keyword args to attach extra metadata to the propagated signal:

class BatchProcessor < CMDx::Task
  def work
    step_result = FileValidation.execute(context)

    if step_result.failed?
      throw!(
        step_result,
        batch_stage: "validation",
        can_retry: true,
        next_step: "file_repair"
      )
    end

    continue_batch
  end
end

Chain Analysis

Fault exposes the originating Result via fault.result, plus the full chain it belongs to. From either, you can walk failure propagation with origin, caused_failure, and threw_failure. See Result - Chain Analysis for the full API.

begin
  DocumentWorkflow.execute!(document_data: data)
rescue CMDx::Fault => e
  puts "Originated by #{e.task}: #{e.message}"
  puts "Root task: #{e.chain.first.task}"     # chain.first is always the root execution
end

# Or via non-bang execute:
result = DocumentWorkflow.execute(document_data: data)
if result.failed?
  origin = result.caused_failure
  puts "Originated by #{origin.task}: #{origin.reason}"
end