{"record":{"id":"342babed0e7a7eeb","repo":"ruby-concurrency/concurrent-ruby","slug":"should-pass-observer-as-a-first-argument-or-block-342bab","errorCode":null,"errorMessage":"should pass observer as a first argument or block","messagePattern":"should pass observer as a first argument or block","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/concurrent-ruby/concurrent/collection/copy_on_write_observer_set.rb","lineNumber":21,"sourceCode":"module Concurrent\n  module Collection\n\n    # A thread safe observer set implemented using copy-on-write approach:\n    # every time an observer is added or removed the whole internal data structure is\n    # duplicated and replaced with a new one.\n    #\n    # @api private\n    class CopyOnWriteObserverSet < Synchronization::LockableObject\n\n      def initialize\n        super()\n        synchronize { ns_initialize }\n      end\n\n      # @!macro observable_add_observer\n      def add_observer(observer = nil, func = :update, &block)\n        if observer.nil? && block.nil?\n          raise ArgumentError, 'should pass observer as a first argument or block'\n        elsif observer && block\n          raise ArgumentError.new('cannot provide both an observer and a block')\n        end\n\n        if block\n          observer = block\n          func = :call\n        end\n\n        synchronize do\n          new_observers = @observers.dup\n          new_observers[observer] = func\n          @observers = new_observers\n          observer\n        end\n      end\n\n      # @!macro observable_delete_observer","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/ruby-concurrency/concurrent-ruby/blob/0b88d5ff75f69b3740c8f0868e76f833cb2fd45d/lib/concurrent-ruby/concurrent/collection/copy_on_write_observer_set.rb#L3-L39","documentation":"`CopyOnWriteObserverSet#add_observer(observer = nil, func = :update, &block)` demands either an observer object or a block; called with neither it raises ArgumentError immediately. It is the copy-on-write twin of CopyOnNotifyObserverSet — identical validation, different mutation strategy (it duplicates the observer hash on each add so iteration never locks) — and both back Concurrent::Observable, so the same misuse surfaces from any observable class. Supplying both observer and block is the mirrored error.","triggerScenarios":"`observable.add_observer` with no args; `add_observer(nil)`; passing an unset variable (`add_observer(callback)` where `callback` stayed nil). Legal: `add_observer(obs)`, `add_observer(obs, :on_change)`, `add_observer { |*a| ... }`.","commonSituations":"Optional listener registration driven by config hashes with absent keys; DSL builders that attach observers before constructing them; nil propagated from `params[:observer]` or service lookups.","solutions":["Pass a real observer object: `add_observer(MyObserver.new)`.","Or pass a block: `add_observer { |time, value| react(value) }`.","Guard at the boundary: `add_observer(obs) if obs` or `obs ||= NOOP_OBSERVER` before calling.","Add a presence check in your registration helper so the failure names the missing callback."],"exampleFix":"// before\nregistry.add_observer(options[:listener]) # options[:listener] is nil\n\n// after\nlistener = options[:listener] || NOOP_LISTENER\nregistry.add_observer(listener)","handlingStrategy":"validation","validationCode":"def register(set, observer)\n  raise ArgumentError, 'observer required' unless observer\n  set.add_observer(observer)\nend\n\nregister(set, options[:listener] || NOOP_LISTENER)","typeGuard":"def observer_supplied?(observer = nil, &block)\n  !observer.nil? || !block.nil?\nend","tryCatchPattern":"begin\n  set.add_observer(obs)\nrescue ArgumentError => e\n  raise unless e.message.include?('observer as a first argument')\n  set.add_observer(&DEFAULT_HANDLER)\nend","preventionTips":["Default or require the listener before registration.","Keep nil out of the pipeline: normalize at the config boundary.","Decide up front between object form (func callback) and block form; do not mix."],"tags":["concurrent-ruby","observer","observable","copy-on-write","argumenterror"],"backgroundTag":"missing-required-argument","analyzedSha":"0b88d5ff75f69b3740c8f0868e76f833cb2fd45d","analyzedAt":"2026-08-21T20:12:56.291Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}