ruby-concurrency/concurrent-ruby · error · ArgumentError

no block given

Error message

no block given

What it means

SimpleExecutorService is a no-pooling executor intended for testing and debugging; it spawns one new thread per task. Its class-level .post requires the task as a block and raises ArgumentError 'no block given' when called without one, before any thread is created.

Source

Thrown at lib/concurrent-ruby/concurrent/executor/simple_executor_service.rb:25

module Concurrent

  # An executor service in which every operation spawns a new,
  # independently operating thread.
  #
  # This is perhaps the most inefficient executor service in this
  # library. It exists mainly for testing an debugging. Thread creation
  # and management is expensive in Ruby and this executor performs no
  # resource pooling. This can be very beneficial during testing and
  # debugging because it decouples the using code from the underlying
  # executor implementation. In production this executor will likely
  # lead to suboptimal performance.
  #
  # @note Intended for use primarily in testing and debugging.
  class SimpleExecutorService < RubyExecutorService

    # @!macro executor_service_method_post
    def self.post(*args)
      raise ArgumentError.new('no block given') unless block_given?
      Thread.new(*args) do
        Thread.current.abort_on_exception = false
        yield(*args)
      end
      true
    end

    # @!macro executor_service_method_left_shift
    def self.<<(task)
      post(&task)
      self
    end

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

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Pass a block: Concurrent::SimpleExecutorService.post { do_work }
  2. If the task is stored in a variable, convert it: .post(&callable)
  3. Prefer the instance API (.new.post) when you need lifecycle control, and pass the block the same way

Example fix

# before
work = -> { crunch_numbers }
Concurrent::SimpleExecutorService.post(work)

# after
work = -> { crunch_numbers }
Concurrent::SimpleExecutorService.post(&work)
Defensive patterns

Strategy: validation

Validate before calling

raise ArgumentError, 'task block required' unless block_given?
Concurrent::SimpleExecutorService.post { yield }

Type guard

->(obj) { obj.respond_to?(:call) } # then .post(&obj)

Try / catch

begin
  Concurrent::SimpleExecutorService.post(&task)
rescue ArgumentError
  raise if block_given? # real bug
  logger.error('post called without a task block')
end

Prevention

When it happens

Trigger: Concurrent::SimpleExecutorService.post(callable) passing a Proc positionally; calling .post with only args (e.g. .post(1, 2)) intending them for the task; copy-pasting instance-style code onto the class interface.

Common situations: Quick test doubles replacing a real thread pool; scripts that store work in lambdas and forget the & when posting; learning the difference between the class-level convenience API and the instance API.

Related errors


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