ruby-concurrency/concurrent-ruby · error · ArgumentError

should pass observer as a first argument or block

Error message

should pass observer as a first argument or block

What it means

`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.

Source

Thrown at lib/concurrent-ruby/concurrent/collection/copy_on_write_observer_set.rb:21

module Concurrent
  module Collection

    # A thread safe observer set implemented using copy-on-write approach:
    # every time an observer is added or removed the whole internal data structure is
    # duplicated and replaced with a new one.
    #
    # @api private
    class CopyOnWriteObserverSet < Synchronization::LockableObject

      def initialize
        super()
        synchronize { ns_initialize }
      end

      # @!macro observable_add_observer
      def add_observer(observer = nil, func = :update, &block)
        if observer.nil? && block.nil?
          raise ArgumentError, 'should pass observer as a first argument or block'
        elsif observer && block
          raise ArgumentError.new('cannot provide both an observer and a block')
        end

        if block
          observer = block
          func = :call
        end

        synchronize do
          new_observers = @observers.dup
          new_observers[observer] = func
          @observers = new_observers
          observer
        end
      end

      # @!macro observable_delete_observer

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Pass a real observer object: `add_observer(MyObserver.new)`.
  2. Or pass a block: `add_observer { |time, value| react(value) }`.
  3. Guard at the boundary: `add_observer(obs) if obs` or `obs ||= NOOP_OBSERVER` before calling.
  4. Add a presence check in your registration helper so the failure names the missing callback.

Example fix

// before
registry.add_observer(options[:listener]) # options[:listener] is nil

// after
listener = options[:listener] || NOOP_LISTENER
registry.add_observer(listener)
Defensive patterns

Strategy: validation

Validate before calling

def register(set, observer)
  raise ArgumentError, 'observer required' unless observer
  set.add_observer(observer)
end

register(set, options[:listener] || NOOP_LISTENER)

Type guard

def observer_supplied?(observer = nil, &block)
  !observer.nil? || !block.nil?
end

Try / catch

begin
  set.add_observer(obs)
rescue ArgumentError => e
  raise unless e.message.include?('observer as a first argument')
  set.add_observer(&DEFAULT_HANDLER)
end

Prevention

When it happens

Trigger: `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| ... }`.

Common situations: 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.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


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