collectiveidea/interactor · error · Interactor::Failure

#<Interactor::Context foo="baz">

Error message

#<Interactor::Context foo="baz">

What it means

Interactor::Failure is the control-flow signal of the interactor gem, not a crash. Interactor::Context#fail! (lib/interactor/context.rb:127-131) merges its hash argument into the current context, marks it failed by setting @failure = true, then raises Failure with the context attached (raise Failure, self). The message #<Interactor::Context foo="baz"> is the inspect output of that failed context, i.e. the keys present when fail! was signaled, such as from context.fail!(foo: "baz"). The exception reaches your code only through the bang entry points: MyInteractor.call! / run! re-raise it (lib/interactor.rb:75-77, 144-152), while plain .call swallows the Failure for its own context and returns the failed context instead (lib/interactor.rb:114-120).

Source

Thrown at lib/interactor/context.rb:130

    # context - A Hash whose key/value pairs are merged into the existing
    #           Interactor::Context instance. (default: {})
    #
    # Examples
    #
    #   context = Interactor::Context.new
    #   # => #<Interactor::Context>
    #   context.fail!
    #   # => Interactor::Failure: #<Interactor::Context>
    #   context.fail! rescue false
    #   # => false
    #   context.fail!(foo: "baz")
    #   # => Interactor::Failure: #<Interactor::Context foo="baz">
    #
    # Raises Interactor::Failure initialized with the Interactor::Context.
    def fail!(context = {})
      context.each { |key, value| self[key.to_sym] = value }
      @failure = true
      raise Failure, self
    end

    # Internal: Track that an Interactor has been called. The "called!" method
    # is used by the interactor being invoked with this context. After an
    # interactor is successfully called, the interactor instance is tracked in
    # the context for the purpose of potential future rollback.
    #
    # interactor - An Interactor instance that has been successfully called.
    #
    # Returns nothing.
    def called!(interactor)
      _called << interactor
    end

    # Public: Roll back the Interactor::Context. Any interactors to which this
    # context has been passed and which have been successfully called are asked
    # to roll themselves back by invoking their "rollback" instance methods.
    #

View on GitHub (pinned to c0e0079375)

Solutions

  1. Rescue Interactor::Failure => e at the exact call site and read e.context (an Interactor::Context carrying the keys merged by fail!, plus failure?/success?); rollback already ran by the time you catch it.
  2. If failure is an expected outcome, replace MyInteractor.call!(args) with MyInteractor.call(args) and branch on context.success?, because the non-bang variant returns the failed context instead of raising.
  3. If the failure is unexpected, debug why the interactor reached fail!: the inspect string in the message shows exactly which keys were set at failure time, so check the guard conditions around each fail! call.
  4. In organizers, trust the automatic rollback: run! rescues, calls context.rollback! (reverse order over _called), then re-raises; implement rollback in interactors that need undoing instead of rescuing mid-chain.
  5. Add Interactor::Failure to the excluded_exceptions list of your error reporter (Sentry, Honeybadger) so this control-flow signal is not classified as a crash.

Example fix

# before
result = PlaceOrder.call!(order: order)
# => raises Interactor::Failure: #<Interactor::Context order=... error="out of stock">

# after (option 1: expected failure, use the non-bang call)
result = PlaceOrder.call(order: order)
unless result.success?
  redirect_to cart_path, alert: result.error
end

# after (option 2: keep call!, handle the signal)
begin
  PlaceOrder.call!(order: order)
rescue Interactor::Failure => e
  Rails.logger.warn("order rejected: #{e.context[:error]}")
end
Defensive patterns

Strategy: try-catch

Validate before calling

# Run the preconditions that guard the internal fail! before the bang call
order_placeable = order.persisted? && order.items.any? && order.total_cents.positive?
raise ArgumentError, "order not placeable" unless order_placeable
result = PlaceOrder.call!(order: order)

Type guard

def interactor_failure?(error)
  error.is_a?(Interactor::Failure) && error.context.is_a?(Interactor::Context)
end

# narrow a returned context before treating it as success
def failed_context?(ctx)
  ctx.respond_to?(:failure?) && ctx.failure?
end

Try / catch

begin
  result = PlaceOrder.call!(order: order)
rescue Interactor::Failure => e
  ctx = e.context          # Interactor::Context with the keys merged by fail!
  report_rejection(ctx)    # ctx[:error] / ctx.failure? == true; rollback already ran
rescue StandardError => e
  raise                    # never let a broad rescue swallow real bugs
end

Prevention

When it happens

Trigger: Calling context.fail! (with or without a hash, e.g. context.fail!(foo: "baz")) inside an interactor #call and invoking it via MyInteractor.call!(...) or MyInteractor.new(...).run!. Using an organizer (organize A, B) whose nested interactor calls fail! while the organizer is invoked with .call!. Calling context.fail! directly on an Interactor::Context object. Edge case: a Failure raised with a context object different from the one owned by the receiving interactor is re-raised even under plain .call (object_id check in lib/interactor.rb:117-119).

Common situations: Switching an invocation from .call to .call! (wanting exceptions) without adding rescue Interactor::Failure, so expected business failures (validation, insufficient funds, record not found) surface as 500s in Rails controllers or crashes in Sidekiq jobs. Error reporters (Sentry, Honeybadger) or a bare rescue => e catching Interactor::Failure and filing false crash alerts. Teams migrating from other service-object gems assume call always raises on failure and are surprised that .call returns silently with failure? == true. Rescue blocks that manually undo work which context.rollback! already undid via the automatic rollback in run!.


AI-assisted analysis of collectiveidea/interactor@c0e0079375 (2026-08-23). Data as JSON: /api/errors/d4bbc10db4289e73. Report an issue: GitHub.