ruby-concurrency/concurrent-ruby · error · ArgumentError

rescuers and block are both missing

Error message

rescuers and block are both missing

What it means

Promise#then builds a child promise whose fulfillment callback is either the given block or a rescuer callable given as the first positional argument (optionally an executor as second argument or via an options hash). If neither a rescuer nor a block is supplied, there is no callback to chain, so it raises ArgumentError 'rescuers and block are both missing'.

Source

Thrown at lib/concurrent-ruby/concurrent/promise.rb:324

    #     promise is rejected.
    #   @param [ThreadPool] executor An optional thread pool executor to be used
    #     in the new Promise
    # @overload then(rescuer, executor: executor, &block)
    #   @param [Proc] rescuer An optional rescue block to be executed if the
    #     promise is rejected.
    #   @param [ThreadPool] executor An optional thread pool executor to be used
    #     in the new Promise
    def then(*args, &block)
      if args.last.is_a?(::Hash)
        executor = args.pop[:executor]
        rescuer = args.first
      else
        rescuer, executor = args
      end

      executor ||= @executor

      raise ArgumentError.new('rescuers and block are both missing') if rescuer.nil? && !block_given?
      block = Proc.new { |result| result } unless block_given?
      child = Promise.new(
        parent: self,
        executor: executor,
        on_fulfill: block,
        on_reject: rescuer
      )

      synchronize do
        child.state = :pending if @state == :pending
        child.on_fulfill(apply_deref_options(@value)) if @state == :fulfilled
        child.on_reject(@reason) if @state == :rejected
        @children << child
      end

      child
    end

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Give then a block: promise.then { |v| v + 1 }
  2. Or a rescuer callable: promise.then(->(reason) { handle(reason) })
  3. When specifying an executor, still supply the callback: promise.then(executor: pool) { |v| v + 1 }

Example fix

# before
child = promise.then(executor: pool)

# after
child = promise.then(executor: pool) { |v| v + 1 }
Defensive patterns

Strategy: validation

Validate before calling

raise ArgumentError, 'callback block or rescuer required' unless block_given? || rescuer
promise.then(on_fulfill_options) { |v| yield(v) }

Type guard

->(obj) { obj.respond_to?(:call) } # valid rescuer for then(rescuer)

Try / catch

begin
  promise.then(executor: executor, &callback)
rescue ArgumentError => e
  raise ArgumentError, 'then needs a block or rescuer' if /rescuers and block/.match?(e.message)
  raise
end

Prevention

When it happens

Trigger: Bare promise.then; promise.then(executor: my_pool) — the options hash is consumed for the executor and no callback remains, which still raises; passing only args that are meant for the block before the block is written.

Common situations: Starting a chain and forgetting the callback mid-edit; passing executor options intending to configure the child without yet adding logic; refactoring then { |v| v }.then(...) chains down to a bare then.

Related errors


AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21). Data as JSON: /api/errors/5133917d2906b367. Report an issue: GitHub.