ruby-concurrency/concurrent-ruby · error · ArgumentError

no block given

Error message

no block given

What it means

MVar is a synchronized single-slot container (empty/full states). #modify atomically takes the current value, yields it to your block, and puts the transformed value back — the block is essential, so calling modify (with or without a timeout) without one raises ArgumentError 'no block given' before any locking happens.

Source

Thrown at lib/concurrent-ruby/concurrent/mvar.rb:124

        # If we timed out we won't be empty
        if unlocked_empty?
          @value = value
          @full_condition.signal
          apply_deref_options(value)
        else
          TIMEOUT
        end
      end
    end

    # Atomically `take`, yield the value to a block for transformation, and then
    # `put` the transformed value. Returns the pre-transform value. A timeout can
    # be set to limit the time spent blocked, in which case it returns `TIMEOUT`
    # if the time is exceeded.
    # @return [Object] the pre-transform value, or `TIMEOUT`
    def modify(timeout = nil)
      raise ArgumentError.new('no block given') unless block_given?

      @mutex.synchronize do
        wait_for_full(timeout)

        # If we timed out we'll still be empty
        if unlocked_full?
          value = @value
          @value = yield value
          @full_condition.signal
          apply_deref_options(value)
        else
          TIMEOUT
        end
      end
    end

    # Non-blocking version of `take`, that returns `EMPTY` instead of blocking.
    def try_take!

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Always pass the transformation block: mvar.modify { |v| v + 1 }
  2. Remember the only positional argument is the timeout: mvar.modify(2) { |v| v + 1 }
  3. If the transformation is stored, pass it as a block: mvar.modify(&transform)

Example fix

# before
mvar.modify(2) # intended 'set to 2 after 2s'

# after
mvar.put(2)                        # plain overwrite (blocks when full)
mvar.modify(2) { |v| v * 10 }      # 2 = timeout, block = transform
Defensive patterns

Strategy: validation

Validate before calling

raise ArgumentError, 'transform block required' unless block_given?
mvar.modify(timeout) { |v| yield(v) }

Type guard

->(obj) { obj.respond_to?(:call) } # then mvar.modify(&obj)

Try / catch

begin
  mvar.modify(timeout) { |v| transform.call(v) }
rescue ArgumentError => e
  raise ArgumentError, 'modify needs a transform block' if /no block/.match?(e.message)
  raise
end

Prevention

When it happens

Trigger: mvar.modify with no block; mvar.modify(5) where 5 was intended as a new value rather than the timeout; helper methods that accept a timeout and a block but drop the & forwarding.

Common situations: Expecting modify(value) to behave like put(value); wrapping MVar in repository-style APIs where the transformation lives in a stored proc; first contact with MVar's Haskell-style API where every operation is block-based.

Related errors


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