ruby-concurrency/concurrent-ruby · error · ArgumentError

no block given

Error message

no block given

What it means

Concurrent.dataflow, dataflow!, dataflow_with and dataflow_with! all funnel into the private call_dataflow, which requires a block describing the computation to run once every input IVar resolves. It raises ArgumentError('no block given') when that block is missing. This is a fail-fast API contract check at call time, not a concurrency failure.

Source

Thrown at lib/concurrent-ruby/concurrent/dataflow.rb:58

    call_dataflow(:value, executor, *inputs, &block)
  end
  module_function :dataflow_with

  def dataflow!(*inputs, &block)
    dataflow_with!(Concurrent.global_io_executor, *inputs, &block)
  end
  module_function :dataflow!

  def dataflow_with!(executor, *inputs, &block)
    call_dataflow(:value!, executor, *inputs, &block)
  end
  module_function :dataflow_with!

  private

  def call_dataflow(method, executor, *inputs, &block)
    raise ArgumentError.new('an executor must be provided') if executor.nil?
    raise ArgumentError.new('no block given') unless block_given?
    unless inputs.all? { |input| input.is_a? IVar }
      raise ArgumentError.new("Not all dependencies are IVars.\nDependencies: #{ inputs.inspect }")
    end

    result = Future.new(executor: executor) do
      values = inputs.map { |input| input.send(method) }
      block.call(*values)
    end

    if inputs.empty?
      result.execute
    else
      counter = DependencyCounter.new(inputs.size) { result.execute }

      inputs.each do |input|
        input.add_observer counter
      end
    end

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Pass the computation as a literal block: Concurrent.dataflow(f1, f2) { |a, b| a + b }
  2. When the callable is in a variable, pass it with &: Concurrent.dataflow(f1, &handler)
  3. In wrapper methods, check block_given? before delegating and raise your own descriptive error
  4. When dispatching dynamically, capture the block as &proc and forward it intact

Example fix

# before
Concurrent.dataflow(future_a, future_b, ->(a, b) { a + b })
# after
Concurrent.dataflow(future_a, future_b) { |a, b| a + b }
Defensive patterns

Strategy: validation

Validate before calling

fail ArgumentError, 'dataflow requires a computation block' unless task.respond_to?(:call)
Concurrent.dataflow(*inputs, &task)

Try / catch

begin
  Concurrent.dataflow(*inputs) { |*vs| compute(*vs) }
rescue ArgumentError => e
  raise unless e.message == 'no block given'
  raise ArgumentError, "dataflow called without a block from #{caller.first}"
end

Prevention

When it happens

Trigger: Calling Concurrent.dataflow(f1, f2) with no block; passing the computation as a positional argument instead of a block (Concurrent.dataflow(f1, ->(v) { v + 1 })); delegating through your own wrapper method that takes &block but whose caller supplied none; dynamic dispatch (define_method/instance_exec) that drops the block.

Common situations: Refactoring drops the trailing block; the callable is stored in a local and passed without the & sigil; a DSL wrapper makes the block optional while dataflow requires it; copy-pasted example code using method objects where no block reaches dataflow.

Related errors


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