ruby-concurrency/concurrent-ruby · error · ArgumentError

no block given

Error message

no block given

What it means

Concurrent::Delay defers a computation until #value is first called; the block given to new IS that computation, so it is mandatory. initialize raises ArgumentError('no block given') when the Delay is constructed without one, because a Delay with nothing to compute is meaningless.

Source

Thrown at lib/concurrent-ruby/concurrent/delay.rb:63

    include Concern::Obligation

    # NOTE: Because the global thread pools are lazy-loaded with these objects
    # there is a performance hit every time we post a new task to one of these
    # thread pools. Subsequently it is critical that `Delay` perform as fast
    # as possible post-completion. This class has been highly optimized using
    # the benchmark script `examples/lazy_and_delay.rb`. Do NOT attempt to
    # DRY-up this class or perform other refactoring with running the
    # benchmarks and ensuring that performance is not negatively impacted.

    # Create a new `Delay` in the `:pending` state.
    #
    # @!macro executor_and_deref_options
    #
    # @yield the delayed operation to perform
    #
    # @raise [ArgumentError] if no block is given
    def initialize(opts = {}, &block)
      raise ArgumentError.new('no block given') unless block_given?
      super(&nil)
      synchronize { ns_initialize(opts, &block) }
    end

    # Return the value this object represents after applying the options
    # specified by the `#set_deref_options` method. If the delayed operation
    # raised an exception this method will return nil. The exception object
    # can be accessed via the `#reason` method.
    #
    # @param [Numeric] timeout the maximum number of seconds to wait
    # @return [Object] the current value of the object
    #
    # @!macro delay_note_regarding_blocking
    def value(timeout = nil)
      if @executor # TODO (pitr 12-Sep-2015): broken unsafe read?
        super
      else
        # this function has been optimized for performance and

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Pass the computation as a block: Concurrent::Delay.new { expensive_call }
  2. Pass a stored callable with &: Concurrent::Delay.new(&task)
  3. In factories, fail fast with raise unless task.respond_to?(:call) before constructing

Example fix

# before
delay = Concurrent::Delay.new(load_config)
# after
delay = Concurrent::Delay.new { load_config }
Defensive patterns

Strategy: validation

Validate before calling

raise ArgumentError, 'task must respond to #call' unless task.respond_to?(:call)
Concurrent::Delay.new(&task)

Try / catch

begin
  Concurrent::Delay.new { compute }
rescue ArgumentError => e
  raise unless e.message == 'no block given'
  Concurrent::Delay.new { default_value }
end

Prevention

When it happens

Trigger: Concurrent::Delay.new with no block at all; Concurrent::Delay.new(timeout: 5) passing only options; passing the task as a positional argument (Concurrent::Delay.new(my_proc)) instead of Concurrent::Delay.new(&my_proc).

Common situations: Factory methods build a Delay from a stored proc and forget the &; the chosen task is conditionally nil; refactoring an eager call into lazy evaluation and dropping the block in the process.

Related errors


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