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.incrementView on GitHub (pinned to 0b88d5ff75)
Solutions
- Pass a block: Concurrent::SimpleExecutorService.post { do_work }
- If the task is stored in a variable, convert it: .post(&callable)
- 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
- Treat SimpleExecutorService as test-only scaffolding; in production prefer global_io_executor/global_fast_executor
- Route all posts through helper methods that assert a block exists
- Convert stored lambdas with & consistently across the codebase
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.