ruby-concurrency/concurrent-ruby · error · ArgumentError

no block given

Error message

no block given

What it means

Concurrent::ScheduledTask executes a block after a delay; the block passed to ScheduledTask.new (or ScheduledTask.execute) is the task body and there is no option-based alternative for supplying it. The constructor raises ArgumentError('no block given') immediately when no block is attached. The task cannot be provided via opts (e.g. opts[:task] is ignored), only via the &block parameter.

Source

Thrown at lib/concurrent-ruby/concurrent/scheduled_task.rb:179

    # The executor on which to execute the task.
    # @!visibility private
    attr_reader :executor

    # Schedule a task for execution at a specified future time.
    #
    # @param [Float] delay the number of seconds to wait for before executing the task
    #
    # @yield the task to be performed
    #
    # @!macro executor_and_deref_options
    #
    # @option opts [object, Array] :args zero or more arguments to be passed the task
    #   block on execution
    #
    # @raise [ArgumentError] When no block is given
    # @raise [ArgumentError] When given a time that is in the past
    def initialize(delay, opts = {}, &task)
      raise ArgumentError.new('no block given') unless block_given?
      raise ArgumentError.new('seconds must be greater than zero') if delay.to_f < 0.0

      super(NULL, opts, &nil)

      synchronize do
        ns_set_state(:unscheduled)
        @parent = opts.fetch(:timer_set, Concurrent.global_timer_set)
        @args = get_arguments_from(opts)
        @delay = delay.to_f
        @task = task
        @time = nil
        @executor = Options.executor_from_options(opts) || Concurrent.global_io_executor
        self.observers = Collection::CopyOnNotifyObserverSet.new
      end
    end

    # The `delay` value given at instantiation.
    #

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Attach the task as a block: Concurrent::ScheduledTask.new(3) { do_work }
  2. When the callable is a variable, forward it explicitly: Concurrent::ScheduledTask.new(3, &my_callable)
  3. If you only need fire-and-forget delayed execution, use the convenience wrapper: Concurrent.schedule(3) { do_work }
  4. Guard construction sites: raise your own descriptive error when the callable is nil so failures surface at the call site, not inside the gem

Example fix

# before
task = Concurrent::ScheduledTask.new(3)

# after
task = Concurrent::ScheduledTask.new(3) { cleanup_temp_files }

# with a stored callable
task = Concurrent::ScheduledTask.new(3, &handler)
Defensive patterns

Strategy: validation

Validate before calling

def build_scheduled_task(delay, &task)
  raise ArgumentError, 'a task block/callable is required' if task.nil?
  Concurrent::ScheduledTask.new(delay, &task)
end

Type guard

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

Try / catch

begin
  Concurrent::ScheduledTask.new(delay) { work }
rescue ArgumentError => e
  raise ContextError, "ScheduledTask for #{job_name}: #{e.message}" if e.message == 'no block given'
  raise
end

Prevention

When it happens

Trigger: Calling Concurrent::ScheduledTask.new(5) or ScheduledTask.execute(5) with no block; storing the task in a local variable and calling new(delay) without forwarding it with &task; refactoring that turns the literal block into a conditional that sometimes yields none.

Common situations: Porting code from Thread.new or TimerTask where the callable was passed differently; building tasks dynamically from config where the proc may be missing; passing a lambda as a positional or opts argument instead of as a block.

Related errors


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