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

CopyOnWriteObserverSet is the second observer-registry implementation behind Concurrent::Observable (it swaps the whole observers hash atomically on change instead of notifying over a copy). Its add_observer has the same contract as the other sets: register an observer object (optional callback method, default :update) or a block, never both — the combination is ambiguous and raises ArgumentError.

Source

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

    # 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
      def delete_observer(observer)
        synchronize do

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Register only the object: `subject.add_observer(listener)` (callback defaults to :update) or `subject.add_observer(listener, :on_event)`.
  2. Or register only the block — it is stored as the observer and called via #call.
  3. If both behaviors are wanted, register them as two separate observers.

Example fix

// before
subject.add_observer(listener) { |e| on(e) }

// after — object form
subject.add_observer(listener, :on)

// or block form
subject.add_observer { |e| on(e) }
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
end

Try / catch

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

Prevention

When it happens

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

Common situations: Switching subjects between observer-set implementations and carrying over a double registration; helpers that always attach a block while callers also pass an object.

Related errors


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