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? #=> falseView on GitHub (pinned to 0b88d5ff75)
Solutions
- Always attach the block: Concurrent::TimerTask.new(execution_interval: 5) { poll_api }
- Forward stored callables with &: Concurrent::TimerTask.new(opts, &job)
- 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
- Always pass the job as a block; never assume opts can carry it
- In timer factories, require &job and fail with a domain-specific message
- Smoke-test config-driven timer setup with a missing job to confirm your guard fires
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.