ruby-concurrency/concurrent-ruby · error · ArgumentError

cannot provide both an observer and a block

Error message

cannot provide both an observer and a block

What it means

CopyOnNotifyObserverSet is the observer registry used by Concurrent::Observable subjects. add_observer accepts either an observer object (with an optional callback method name, default :update) or a block, but not both — passing both is ambiguous about what should be notified, so it raises ArgumentError before anything is registered.

Source

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

    # 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
          @observers.delete(observer)
          observer

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Remove the block when registering an object: `subject.add_observer(listener)` — the callback defaults to listener.update.
  2. Or name the object's callback method: `subject.add_observer(listener, :on_change)`.
  3. Or drop the object and keep only the block — the block itself becomes the observer and is invoked via #call.

Example fix

// before
chat.add_observer(logger) { |msg| log(msg) }

// after — object form
chat.add_observer(logger, :log)

// or block form
chat.add_observer { |msg| log(msg) }
Defensive patterns

Strategy: validation

Validate before calling

def register(subject, observer = nil, func = :update, &block)
  raise ArgumentError, 'observer OR block, not both' if observer && block
  subject.add_observer(observer, func, &block)
end

Type guard

def valid_registration?(observer, block_given)
  observer.nil? != block_given # exactly one form supplied
end

Try / catch

begin
  subject.add_observer(listener) { |m| log(m) }
rescue ArgumentError => e
  raise unless e.message.include?('observer and a block')
  subject.add_observer(listener, :log)
end

Prevention

When it happens

Trigger: `subject.add_observer(listener) { |value| handle(value) }`; `observer_set.add_observer(obj, :on_change) { ... }` — any call supplying both the positional observer and a block.

Common situations: Refactoring from block callbacks to object callbacks (or vice versa) and leaving the other form behind; adding a block 'for documentation' while also registering an object; wrapper methods that always append a block.

Related errors


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