ruby-concurrency/concurrent-ruby · error · ArgumentError

Cannot use both value and block as default value

Error message

Cannot use both value and block as default value

What it means

`Concurrent::FiberLocalVar.new(default = nil, &default_block)` accepts exactly one default mechanism: a static value or a block evaluated per fiber on first read there. The guard is `default && block_given?`, so a truthy default plus a block raises ArgumentError, while `nil` (or `false`) plus a block is silently allowed and the block wins. The conflict is rejected because the resolution order between a static value and a computed block would be ambiguous.

Source

Thrown at lib/concurrent-ruby/concurrent/atomic/fiber_local_var.rb:51

  #
  #   Fiber.new do
  #     v.value #=> 14
  #     v.value = 2
  #     v.value #=> 2
  #   end.resume
  #
  #   v.value #=> 14
  class FiberLocalVar
    LOCALS = FiberLocals.new

    # Creates a fiber local variable.
    #
    # @param [Object] default the default value when otherwise unset
    # @param [Proc] default_block Optional block that gets called to obtain the
    #   default value for each fiber
    def initialize(default = nil, &default_block)
      if default && block_given?
        raise ArgumentError, "Cannot use both value and block as default value"
      end

      if block_given?
        @default_block = default_block
        @default = nil
      else
        @default_block = nil
        @default = default
      end

      @index = LOCALS.next_index(self)
    end

    # Returns the value in the current fiber's copy of this fiber-local variable.
    #
    # @return [Object] the current value
    def value
      LOCALS.fetch(@index) { default }

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Pick one mechanism: `FiberLocalVar.new { per_fiber_default }` for computed defaults or `FiberLocalVar.new(value)` for constants.
  2. In wrappers, forward only the winner: `blk ? FiberLocalVar.new(&blk) : FiberLocalVar.new(value)`.
  3. Remember the asymmetry: only truthy values conflict — `nil`/`false` with a block quietly uses the block, which can mask intent.

Example fix

// before
req_id = Concurrent::FiberLocalVar.new('none') { Fiber.current.object_id.to_s(36) }

// after
req_id = Concurrent::FiberLocalVar.new { Fiber.current.object_id.to_s(36) }
Defensive patterns

Strategy: validation

Validate before calling

def fiber_local(default = nil, &blk)
  raise ArgumentError, 'default value or block, not both' if default && blk
  Concurrent::FiberLocalVar.new(default, &blk)
end

Prevention

When it happens

Trigger: `Concurrent::FiberLocalVar.new('n/a') { Fiber.current.object_id }` — truthy string plus block. Configuration DSLs that always capture a block while also passing a fallback positional. Wrappers forwarding both `default` and `&blk` from their own signature.

Common situations: A config object that gained a per-fiber block default later while keeping the old value argument; copy-paste between ThreadLocalVar and FiberLocalVar where both were being 'combined'; helpers that accept `value = nil, &blk` and forward both unconditionally.

Related errors


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