Interruptions - Faults¶
CMDx::Fault is the exception you get from execute! when a task fails. Skips and successes never become faults.
A fault wraps the originating failed Result — the leaf at the bottom of any throw! chain — and delegates task, context, and chain from that result. For the bigger picture on errors, see Exceptions.
What's on a Fault¶
| Accessor | Returns | Notes |
|---|---|---|
fault.result |
CMDx::Result |
The failed result that started the problem (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 |
Every result produced during the run |
fault.message |
String |
I18nProxy.tr(result.reason) — translates i18n keys, otherwise uses the string as-is; falls back to localized cmdx.reasons.unspecified when reason is nil |
fault.backtrace |
Array<String> |
From result.backtrace or result.cause&.backtrace_locations, cleaned with task.settings.backtrace_cleaner when set |
For day-to-day debugging, read fault.result for reason, metadata, cause, origin, state, and status.
Note
Faults cover failures from fail!, throw!, or errors.add. If the runtime rescued a plain StandardError and stored it on the result, execute! re-raises that original exception instead of a Fault. In workflows, fault.task always points at the leaf that failed, so Fault.for?(LeafTask) behaves the same for simple and nested runs.
Fault Handling¶
Bang form: rescue and log or notify.
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
Non-bang form: keep the Result and branch on it.
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
Same facts, two doors: execute! gives you fault.result / fault.context / fault.chain; execute gives you the result directly.
Advanced Matching¶
Task-Specific Matching¶
Fault.for?(*task_classes) builds a tiny matcher class you can use in rescue. It matches when fault.task is one of the listed classes (or inherits from one).
begin
DocumentWorkflow.execute!(document_data: data)
rescue CMDx::Fault.for?(FormatValidator, ContentProcessor) => e
# Only document pipeline failures land here
retry_with_alternate_parser(e.result.metadata)
end
Reason-Specific Matching¶
Fault.reason?(reason) matches when result.reason equals the string you passed.
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? runs your block on the fault. Use it for metadata, status, cause class, anything you can express in Ruby.
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 for? / matches? call returns a fresh anonymous class. Great for stacking rescue lines; not great for merging into one mega-matcher.
Fault Propagation¶
throw! forwards another task's failed result through the current task. The signal copies state, status, and reason, and attaches the current caller_locations as the backtrace. If the argument is not failed, throw! does nothing — it never turns a skip or success into a failure.
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¶
Keyword arguments merge into this task's result metadata on top of the propagated failure.
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.result is the originating failure; fault.chain is the whole story. Walk propagation with origin, caused_failure, and threw_failure. Full tour in Result - Chain Analysis.
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