ruby-concurrency/concurrent-ruby · error · ArgumentError

no block given

Error message

no block given

What it means

TimerTask is a recurring background job: the block given to TimerTask.new (or TimerTask.execute) is executed every execution_interval seconds and is mandatory. The constructor raises ArgumentError('no block given') without it. There is no opts-based way to hand it the task.

Source

Thrown at lib/concurrent-ruby/concurrent/timer_task.rb:211

    #   @options opts [Symbol] :interval_type method to calculate the interval
    #     between executions, can be either :fixed_rate or :fixed_delay.
    #     (default: :fixed_delay)
    #   @option opts [Executor] executor, default is `global_io_executor`
    #
    #   @!macro deref_options
    #
    #   @raise ArgumentError when no block is given.
    #
    #   @yield to the block after :execution_interval seconds have passed since
    #     the last yield
    #   @yieldparam task a reference to the `TimerTask` instance so that the
    #     block can control its own lifecycle. Necessary since `self` will
    #     refer to the execution context of the block rather than the running
    #     `TimerTask`.
    #
    #   @return [TimerTask] the new `TimerTask`
    def initialize(opts = {}, &task)
      raise ArgumentError.new('no block given') unless block_given?
      super
      set_deref_options opts
    end

    # Is the executor running?
    #
    # @return [Boolean] `true` when running, `false` when shutting down or shutdown
    def running?
      @running.true?
    end

    # Execute a previously created `TimerTask`.
    #
    # @return [TimerTask] a reference to `self`
    #
    # @example Instance and execute in separate steps
    #   task = Concurrent::TimerTask.new(execution_interval: 10){ print "Hello World\n" }
    #   task.running? #=> false

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Always attach the block: Concurrent::TimerTask.new(execution_interval: 5) { poll_api }
  2. Forward stored callables with &: Concurrent::TimerTask.new(opts, &job)
  3. For a single delayed execution use ScheduledTask/Concurrent.schedule instead of a self-cancelling TimerTask

Example fix

# before
task = Concurrent::TimerTask.new(execution_interval: 10)

# after
task = Concurrent::TimerTask.new(execution_interval: 10) { poll_health_endpoint }
Defensive patterns

Strategy: validation

Validate before calling

raise ArgumentError, 'TimerTask requires a job block' unless block_given?
Concurrent::TimerTask.new(opts, &job)

Type guard

def job_callable?(obj)
  obj.respond_to?(:call)
end

Try / catch

begin
  Concurrent::TimerTask.new(opts, &job)
rescue ArgumentError => e
  raise ConfigError, "timer '#{name}' has no job block" if e.message == 'no block given'
  raise
end

Prevention

When it happens

Trigger: Concurrent::TimerTask.new(execution_interval: 5) with no block; TimerTask.execute(interval) { } forgotten block variant; factory methods that receive the task as a positional lambda instead of a block.

Common situations: Building periodic workers from configuration where the job proc may be missing; refactors that move the block body into a variable and forget the & prefix; smoke tests constructing TimerTask just to check options validation.

Related errors


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