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

`CopyOnNotifyObserverSet#add_observer(observer = nil, func = :update, &block)` requires either an observer object (on which `func`, default `:update`, will be called) or a block (invoked via `:call`). Called with neither — bare `add_observer` or `add_observer(nil)` — it raises ArgumentError before touching the set. This set is one of the two observer strategies behind Concurrent::Observable; the sibling error 'cannot provide both an observer and a block' guards the opposite misuse.

Source

Thrown at lib/concurrent-ruby/concurrent/collection/copy_on_notify_observer_set.rb:22

  module Collection

    # A thread safe observer set implemented using copy-on-read approach:
    # observers are added and removed from a thread safe collection; every time
    # a notification is required the internal data structure is copied to
    # prevent concurrency issues
    #
    # @api private
    class CopyOnNotifyObserverSet < 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
          @observers[observer] = func
          observer
        end
      end

      # @!macro observable_delete_observer
      def delete_observer(observer)
        synchronize do

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Pass the observer object: `add_observer(MyObserver.new)`.
  2. Or pass a block: `add_observer { |*args| handle(*args) }`.
  3. Guard optionality at the call site: `add_observer(obs) if obs`, or default the observer (`obs ||= DefaultObserver.new`).
  4. In wrapper APIs, raise your own descriptive error when both observer and block are missing.

Example fix

// before
add_observer(@listener) # @listener is nil from missing config

// after
add_observer(@listener) if @listener
# or always attach a handler:
add_observer { |*args| @listener&.call(*args) }
Defensive patterns

Strategy: validation

Validate before calling

def attach(set, observer = nil, func = :update, &block)
  return set.add_observer(observer, func, &block) if observer || block
  raise ArgumentError, 'observer object or block required'
end

# usage
attach(set, obs) if obs
attach(set) { |*a| handle(*a) } if obs.nil?

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 { |*args| fallback.call(*args) }
end

Prevention

When it happens

Trigger: `observable.add_observer` with no arguments; `add_observer(nil)`; forwarding a variable that was never assigned: `add_observer(@observer)` when config omitted the key. `add_observer(nil, &blk)` is fine (block path).

Common situations: Optional-callback APIs where the observer comes from config or params and can be absent; builder DSLs that defer attaching and accidentally attach nil; refactor that moved observer construction after the attach call.

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/08c3a086a813ceb0. Report an issue: GitHub.