Interruptions - Halt¶
Stop task execution intentionally using skip! or fail!. Both methods signal clear intent about why execution stopped.
Skipping¶
Use skip! when the task doesn't need to run. It's a no-op, not an error.
Important
Skipped tasks are considered "good" outcomes—they succeeded by doing nothing.
class ProcessInventory < CMDx::Task
def work
# Without a reason
skip! if Array(ENV["DISABLED_TASKS"]).include?(self.class.name)
# With a reason
skip!("Warehouse closed") unless Time.now.hour.between?(8, 18)
inventory = Inventory.find(context.inventory_id)
if inventory.already_counted?
skip!("Inventory already counted today")
else
inventory.count!
end
end
end
result = ProcessInventory.execute(inventory_id: 456)
# Executed
result.status #=> "skipped"
# Without a reason
result.reason #=> "Unspecified"
# With a reason
result.reason #=> "Warehouse closed"
Failing¶
Use fail! when the task can't complete successfully. It signals controlled, intentional failure:
class ProcessRefund < CMDx::Task
def work
# Without a reason
fail! if Array(ENV["DISABLED_TASKS"]).include?(self.class.name)
refund = Refund.find(context.refund_id)
# With a reason
if refund.expired?
fail!("Refund period has expired")
elsif !refund.amount.positive?
fail!("Refund amount must be positive")
else
refund.process!
end
end
end
result = ProcessRefund.execute(refund_id: 789)
# Executed
result.status #=> "failed"
# Without a reason
result.reason #=> "Unspecified"
# With a reason
result.reason #=> "Refund period has expired"
Metadata Enrichment¶
Enrich halt calls with metadata for better debugging and error handling:
class ProcessRenewal < CMDx::Task
def work
license = License.find(context.license_id)
if license.already_renewed?
# Without metadata
skip!("License already renewed")
end
unless license.renewal_eligible?
# With metadata
fail!(
"License not eligible for renewal",
error_code: "LICENSE.NOT_ELIGIBLE",
retry_after: Time.current + 30.days
)
end
process_renewal
end
end
result = ProcessRenewal.execute(license_id: 567)
# Without metadata
result.metadata #=> {}
# With metadata
result.metadata #=> {
# error_code: "LICENSE.NOT_ELIGIBLE",
# retry_after: <Time 30 days from now>
# }
Idempotency¶
Halt methods are safe to call when the result is already in the target status:
class ProcessOrder < CMDx::Task
def work
fail!("Out of stock") if out_of_stock?
fail!("Insufficient funds") if insufficient_funds?
# Second fail! is a no-op if already failed above
end
end
Important
skip! and fail! are no-ops when the result is already in the matching status. However, calling skip! after fail! (or vice versa) raises a RuntimeError — status transitions are unidirectional.
State Transitions¶
Halt methods trigger specific state and status transitions:
| Method | State | Status | Outcome |
|---|---|---|---|
skip! |
interrupted |
skipped |
good? = true, bad? = true |
fail! |
interrupted |
failed |
good? = false, bad? = true |
result = ProcessRenewal.execute(license_id: 567)
# State information
result.state #=> "interrupted"
result.status #=> "skipped" or "failed"
result.interrupted? #=> true
result.complete? #=> false
# Outcome categorization
result.good? #=> true for skipped, false for failed
result.bad? #=> true for both skipped and failed
Execution Behavior¶
Halt methods behave differently depending on the call method used:
Non-bang execution¶
Returns result object without raising exceptions:
result = ProcessRefund.execute(refund_id: 789)
case result.status
when "success"
puts "Refund processed: $#{result.context.refund.amount}"
when "skipped"
puts "Refund skipped: #{result.reason}"
when "failed"
puts "Refund failed: #{result.reason}"
handle_refund_error(result.metadata[:error_code])
end
Bang execution¶
Raises exceptions for halt conditions based on task_breakpoints configuration:
begin
result = ProcessRefund.execute!(refund_id: 789)
puts "Success: Refund processed"
rescue CMDx::SkipFault => e
puts "Skipped: #{e.message}"
rescue CMDx::FailFault => e
puts "Failed: #{e.message}"
handle_refund_failure(e.result.metadata[:error_code])
end
Best Practices¶
Always provide a reason for better debugging and clearer exception messages:
# Good: Clear, specific reason
skip!("Document processing paused for compliance review")
fail!("File format not supported by processor", code: "FORMAT_UNSUPPORTED")
# Acceptable: Generic, non-specific reason
skip!("Paused")
fail!("Unsupported")
# Bad: Default, cannot determine reason
skip! #=> "Unspecified"
fail! #=> "Unspecified"
Manual Errors¶
For rare cases, manually add errors before halting.
Important
Manual errors don't stop execution—you still need to call fail! or skip!.
Errors API¶
The errors object provides methods for querying and formatting error messages:
errors.add(:email, "is invalid")
errors.add(:email, "is required")
errors.add(:name, "is too short")
errors.any? #=> true
errors.empty? #=> false
errors.size #=> 2 (number of attributes with errors)
errors.for?(:email) #=> true
errors.for?(:phone) #=> false
errors.to_h #=> { email: ["is invalid", "is required"], name: ["is too short"] }
errors.full_messages #=> { email: ["email is invalid", "email is required"], name: ["name is too short"] }
errors.to_s #=> "email is invalid. email is required. name is too short"