ruby-concurrency/concurrent-ruby · error · ArgumentError

no block given

Error message

no block given

What it means

IndirectImmediateExecutor is an ImmediateExecutor that runs the task on an internal SimpleExecutorService thread and blocks the caller on an Event until it finishes, avoiding stack growth from nested immediate execution. post(*args, &task) raises ArgumentError('no block given') when the block is missing.

Source

Thrown at lib/concurrent-ruby/concurrent/executor/indirect_immediate_executor.rb:28

  # immediately runs every `#post` operation on a new thread, blocking the
  # current thread until the operation is complete. This is similar to how the
  # ImmediateExecutor works, but the operation has the full stack of the new
  # thread at its disposal. This can be helpful when the operations will spawn
  # more operations on the same executor and so on - such a situation might
  # overflow the single stack in case of an ImmediateExecutor, which is
  # inconsistent with how it would behave for a threaded executor.
  #
  # @note Intended for use primarily in testing and debugging.
  class IndirectImmediateExecutor < ImmediateExecutor
    # Creates a new executor
    def initialize
      super
      @internal_executor = SimpleExecutorService.new
    end

    # @!macro executor_service_method_post
    def post(*args, &task)
      raise ArgumentError.new("no block given") unless block_given?
      return false unless running?

      event = Concurrent::Event.new
      @internal_executor.post do
        begin
          task.call(*args)
        ensure
          event.set
        end
      end
      event.wait

      true
    end
  end
end

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Always call with a block: executor.post { do_work }
  2. Pass stored callables with &: executor.post(&task)
  3. Check task.respond_to?(:call) before posting in generic dispatch code

Example fix

# before
executor.post(:arg)
# after
executor.post(:arg) { |a| do_work(a) }
Defensive patterns

Strategy: validation

Validate before calling

raise ArgumentError, 'task must respond to #call' unless task.respond_to?(:call)
executor.post(&task)

Try / catch

begin
  executor.post(:arg) { |a| run(a) }
rescue ArgumentError => e
  raise unless e.message == 'no block given'
  raise ArgumentError, 'post requires a block'
end

Prevention

When it happens

Trigger: executor.post with no block; passing args only (executor.post(:job)); forwarding a stored proc without the & sigil.

Common situations: Testing harnesses where a callable is optional and nil slips through; refactoring an executor-swapping layer (tests use IndirectImmediateExecutor, production uses a real pool) that drops the block on one path.

Related errors


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