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::ThreadLocalVar.new(default = nil, &default_block)` accepts exactly one default mechanism: a static value or a block evaluated per thread on first read there. The guard is `default && block_given?`, so a truthy default plus a block raises ArgumentError; `nil` (or `false`) together with a block is silently accepted with the block winning. Thread-identical twin of FiberLocalVar's constructor rule.
Source
Thrown at lib/concurrent-ruby/concurrent/atomic/thread_local_var.rb:53
#
# t2 = Thread.new do
# v.value #=> 14
# v.value = 2
# v.value #=> 2
# end
#
# v.value #=> 14
class ThreadLocalVar
LOCALS = ThreadLocals.new
# Creates a thread 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 thread
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 thread's copy of this thread-local variable.
#
# @return [Object] the current value
def value
LOCALS.fetch(@index) { default }View on GitHub (pinned to 0b88d5ff75)
Solutions
- Choose one mechanism: `ThreadLocalVar.new { per_thread_default }` for computed defaults, `ThreadLocalVar.new(value)` for constants.
- In wrappers, forward only one: `blk ? ThreadLocalVar.new(&blk) : ThreadLocalVar.new(default)`.
- Note the asymmetry: `nil`/`false` plus a block does not raise — the block silently wins.
Example fix
// before
ctx = Concurrent::ThreadLocalVar.new({}) { { request_id: nil } }
// after
ctx = Concurrent::ThreadLocalVar.new { { request_id: nil } } Defensive patterns
Strategy: validation
Validate before calling
def thread_local(default = nil, &blk) raise ArgumentError, 'default value or block, not both' if default && blk Concurrent::ThreadLocalVar.new(default, &blk) end
Prevention
- Pick one default mechanism up front: value or block.
- nil/false plus a block silently uses the block — avoid relying on that.
- Share initializer helpers that enforce the rule across ThreadLocalVar and FiberLocalVar.
When it happens
Trigger: `Concurrent::ThreadLocalVar.new(0) { Thread.current.object_id }` — truthy integer plus block. Wrappers with signature `def var(default = nil, &blk)` forwarding both. Config APIs that pass a fallback value and always capture a block.
Common situations: Per-thread context objects (request IDs, DB connections) that started as a constant default and later gained a computed one; copy-paste initialization lines; sharing initializer code between ThreadLocalVar and FiberLocalVar.
Related errors
- Cannot use both value and block as default value
- 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
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/6a2f5fdf8ad4b845.
Report an issue: GitHub.