ruby-concurrency/concurrent-ruby · error · ArgumentError
no block given
Error message
no block given
What it means
A Concurrent::Future wraps an asynchronous operation supplied at construction time, so Concurrent::Future.new requires a block. Without one it raises ArgumentError 'no block given' at creation — nothing is executed and no Future state is set.
Source
Thrown at lib/concurrent-ruby/concurrent/future.rb:34
# @!macro copy_options
#
# @see http://ruby-doc.org/stdlib-2.1.1/libdoc/observer/rdoc/Observable.html Ruby Observable module
# @see http://clojuredocs.org/clojure_core/clojure.core/future Clojure's future function
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Future.html java.util.concurrent.Future
class Future < IVar
# Create a new `Future` in the `:unscheduled` state.
#
# @yield the asynchronous operation to perform
#
# @!macro executor_and_deref_options
#
# @option opts [object, Array] :args zero or more arguments to be passed the task
# block on execution
#
# @raise [ArgumentError] if no block is given
def initialize(opts = {}, &block)
raise ArgumentError.new('no block given') unless block_given?
super(NULL, opts.merge(__task_from_block__: block), &nil)
end
# Execute an `:unscheduled` `Future`. Immediately sets the state to `:pending` and
# passes the block to a new thread/thread pool for eventual execution.
# Does nothing if the `Future` is in any state other than `:unscheduled`.
#
# @return [Future] a reference to `self`
#
# @example Instance and execute in separate steps
# future = Concurrent::Future.new{ sleep(1); 42 }
# future.state #=> :unscheduled
# future.execute
# future.state #=> :pending
#
# @example Instance and execute in one line
# future = Concurrent::Future.new{ sleep(1); 42 }.execute
# future.state #=> :pendingView on GitHub (pinned to 0b88d5ff75)
Solutions
- Always give Future.new a block: Future.new { compute } — combine with options as Future.new(executor: pool) { compute }
- If the operation is a stored callable, pass it as a block: Future.new(&callable)
- For a write-once placeholder you complete manually, use Concurrent::IVar instead of Future
- For immediate scheduling, use the Concurrent.future { ... } helper
Example fix
# before
op = -> { fetch_data }
f = Concurrent::Future.new(op)
# after
op = -> { fetch_data }
f = Concurrent::Future.new(&op) Defensive patterns
Strategy: validation
Validate before calling
raise ArgumentError, 'operation block required' unless block_given?
Concurrent::Future.new(executor: executor) { yield } Type guard
->(obj) { obj.respond_to?(:call) } # then Future.new(&obj) Try / catch
begin Concurrent::Future.new(&op) rescue ArgumentError => e raise ArgumentError, 'Future needs an operation block' if /no block/.match?(e.message) raise end
Prevention
- Standardize on Concurrent.future { } for immediate use and Future.new(&op) for stored callables
- If you need a manual placeholder, use Concurrent::IVar, not Future
- Lint for Future.new calls with no block in code review
When it happens
Trigger: Concurrent::Future.new(callable) passing a Proc positionally; Concurrent::Future.new(executor: pool) with no operation block; constructing a Future intending to 'fill it in later' (that pattern is IVar).
Common situations: Migrating from hand-rolled Thread.new code where the callable is a variable; passing executor options but forgetting the operation; expecting Future to work like a placeholder container instead of an eager task description.
Related errors
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/6e9daa1003898643.
Report an issue: GitHub.