ruby-concurrency/concurrent-ruby · error · ArgumentError
no block given
Error message
no block given
What it means
TimerSet is the internal executor behind Concurrent::timer and ScheduledTask scheduling. Its #post(delay, *args) takes the delay as the first argument and the task as a block; calling it without a block raises ArgumentError 'no block given'. The delay itself is validated separately by ScheduledTask.execute, which requires a future execution time.
Source
Thrown at lib/concurrent-ruby/concurrent/executor/timer_set.rb:49
super(opts)
end
# Post a task to be execute run after a given delay (in seconds). If the
# delay is less than 1/100th of a second the task will be immediately post
# to the executor.
#
# @param [Float] delay the number of seconds to wait for before executing the task.
# @param [Array<Object>] args the arguments passed to the task on execution.
#
# @yield the task to be performed.
#
# @return [Concurrent::ScheduledTask, false] IVar representing the task if the post
# is successful; false after shutdown.
#
# @raise [ArgumentError] if the intended execution time is not in the future.
# @raise [ArgumentError] if no block is given.
def post(delay, *args, &task)
raise ArgumentError.new('no block given') unless block_given?
return false unless running?
opts = { executor: @task_executor,
args: args,
timer_set: self }
task = ScheduledTask.execute(delay, opts, &task) # may raise exception
task.unscheduled? ? false : task
end
# Begin an immediate shutdown. In-progress tasks will be allowed to
# complete but enqueued tasks will be dismissed and no new tasks
# will be accepted. Has no additional effect if the thread pool is
# not running.
def kill
shutdown
@timer_executor.kill
end
private :<<View on GitHub (pinned to 0b88d5ff75)
Solutions
- Use the public API: Concurrent::timer(2) { do_work } instead of touching TimerSet
- Pass the task as a block after the delay: timer_set.post(2) { do_work }
- If the callable is stored, pass it as a block: timer_set.post(2, &callable)
Example fix
# before
timer_set.post(2)
# after
Concurrent::timer(2) { do_work } Defensive patterns
Strategy: validation
Validate before calling
raise ArgumentError, 'task block required' unless block_given?
Concurrent::timer(delay) { yield } # prefer the public API over TimerSet Type guard
->(d) { d.is_a?(Numeric) && d >= 0 } # delay guard, not a block guard Try / catch
begin
timer_set.post(delay, &task)
rescue ArgumentError => e
raise ArgumentError, "timer post rejected: #{e.message}" unless /no block/.match?(e.message)
raise
end Prevention
- Use Concurrent::timer / Concurrent::scheduled_task instead of touching TimerSet internals
- Assert block_given? in timer wrappers before calling post
- Remember post's signature: post(delay, *args) { |*args| ... }
When it happens
Trigger: timer_set.post(2) with no block; passing the task as a positional argument after the delay: timer_set.post(2, task); forgetting that the first parameter is the delay and leading with the block's args instead.
Common situations: Code reaching into TimerSet directly instead of using the public Concurrent::timer API; porting timer code from other libs where the callable is positional; wrapping timers in adapter classes that drop the &block forwarding.
Related errors
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/1c6bc1a6c0082235.
Report an issue: GitHub.