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

  1. Use the public API: Concurrent::timer(2) { do_work } instead of touching TimerSet
  2. Pass the task as a block after the delay: timer_set.post(2) { do_work }
  3. 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

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.