{"record":{"id":"6a2f5fdf8ad4b845","repo":"ruby-concurrency/concurrent-ruby","slug":"cannot-use-both-value-and-block-as-default-value-6a2f5f","errorCode":null,"errorMessage":"Cannot use both value and block as default value","messagePattern":"Cannot use both value and block as default value","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/concurrent-ruby/concurrent/atomic/thread_local_var.rb","lineNumber":53,"sourceCode":"  #\n  #   t2 = Thread.new do\n  #     v.value #=> 14\n  #     v.value = 2\n  #     v.value #=> 2\n  #   end\n  #\n  #   v.value #=> 14\n  class ThreadLocalVar\n    LOCALS = ThreadLocals.new\n\n    # Creates a thread local variable.\n    #\n    # @param [Object] default the default value when otherwise unset\n    # @param [Proc] default_block Optional block that gets called to obtain the\n    #   default value for each thread\n    def initialize(default = nil, &default_block)\n      if default && block_given?\n        raise ArgumentError, \"Cannot use both value and block as default value\"\n      end\n\n      if block_given?\n        @default_block = default_block\n        @default = nil\n      else\n        @default_block = nil\n        @default = default\n      end\n\n      @index = LOCALS.next_index(self)\n    end\n\n    # Returns the value in the current thread's copy of this thread-local variable.\n    #\n    # @return [Object] the current value\n    def value\n      LOCALS.fetch(@index) { default }","sourceCodeStart":35,"sourceCodeEnd":71,"githubUrl":"https://github.com/ruby-concurrency/concurrent-ruby/blob/0b88d5ff75f69b3740c8f0868e76f833cb2fd45d/lib/concurrent-ruby/concurrent/atomic/thread_local_var.rb#L35-L71","documentation":"`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.","triggerScenarios":"`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.","commonSituations":"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.","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."],"exampleFix":"// before\nctx = Concurrent::ThreadLocalVar.new({}) { { request_id: nil } }\n\n// after\nctx = Concurrent::ThreadLocalVar.new { { request_id: nil } }","handlingStrategy":"validation","validationCode":"def thread_local(default = nil, &blk)\n  raise ArgumentError, 'default value or block, not both' if default && blk\n  Concurrent::ThreadLocalVar.new(default, &blk)\nend","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["concurrent-ruby","threadlocalvar","argumenterror","default-value"],"backgroundTag":"mutually-exclusive-arguments","analyzedSha":"0b88d5ff75f69b3740c8f0868e76f833cb2fd45d","analyzedAt":"2026-08-21T20:12:56.291Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}