ruby-concurrency/concurrent-ruby · error · ArgumentError
an executor must be provided
Error message
an executor must be provided
What it means
Concurrent.dataflow_with / dataflow_with! build a Future whose block runs once the input IVars resolve, scheduled on the executor you pass as the first argument. call_dataflow rejects a nil executor immediately, because scheduling on nothing would otherwise surface later as an obscure failure inside the Future machinery.
Source
Thrown at lib/concurrent-ruby/concurrent/dataflow.rb:57
def dataflow_with(executor, *inputs, &block)
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
endView on GitHub (pinned to 0b88d5ff75)
Solutions
- Pass a real executor: `Concurrent.dataflow_with!(Concurrent.global_io_executor, f1, f2) { |a, b| a + b }` or your own ThreadPoolExecutor.
- Default nil away: `Concurrent.dataflow_with!(executor || Concurrent.global_io_executor, *inputs, &job)`.
- If no custom executor is needed, use plain `Concurrent.dataflow` / `Concurrent.dataflow!`, which run on the global IO executor.
- Note the sibling checks enforced in the same method: a block is required and every input must be an IVar (e.g. a Future or another dataflow result).
Example fix
// before
executor = config[:pool] # nil when key missing
Concurrent.dataflow_with!(executor, f1, f2) { |a, b| a + b }
// after
executor = config.fetch(:pool) { Concurrent.global_io_executor }
Concurrent.dataflow_with!(executor, f1, f2) { |a, b| a + b } Defensive patterns
Strategy: validation
Validate before calling
executor = Concurrent.global_io_executor if executor.nil? Concurrent.dataflow_with!(executor, *inputs, &job)
Type guard
def executor_like?(e) !e.nil? && e.respond_to?(:post) end
Try / catch
begin Concurrent.dataflow_with!(executor, f1, f2, &job) rescue ArgumentError => e raise unless e.message == 'an executor must be provided' Concurrent.dataflow_with!(Concurrent.global_io_executor, f1, f2, &job) end
Prevention
- dataflow_with/dataflow_with! take the executor as the FIRST argument — always supply it explicitly.
- Default unresolved executor config to Concurrent.global_io_executor or a shared ThreadPoolExecutor.
- Remember the sibling validations: a block is required and every input must be an IVar (Future or another dataflow result).
When it happens
Trigger: `Concurrent.dataflow_with!(nil, f1, f2) { |a, b| a + b }`; an executor variable that is nil because configuration was missing or the assigning branch never ran; explicitly passing nil as a placeholder for the executor slot.
Common situations: Executor selection from config/YAML where the key is absent; custom thread pools created conditionally; refactoring between dataflow (which uses Concurrent.global_io_executor) and dataflow_with! and dropping the argument.
Related errors
- unbuffered channels cannot have a capacity
- capacity must be at least 1 for this buffer type
- no block given
- size must be greater than 0
- invalid action
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/4f1cd95386aa3b4b.
Report an issue: GitHub.