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 #=> :pending

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Always give Future.new a block: Future.new { compute } — combine with options as Future.new(executor: pool) { compute }
  2. If the operation is a stored callable, pass it as a block: Future.new(&callable)
  3. For a write-once placeholder you complete manually, use Concurrent::IVar instead of Future
  4. 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

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.