ruby-concurrency/concurrent-ruby · error · ArgumentError

only one error handler allowed

Error message

only one error handler allowed

What it means

A Channel selector stores exactly one error handler (@error_handler), because there is no defined order in which multiple handlers could run. Registering a second one via sel.error raises ArgumentError immediately.

Source

Thrown at lib/concurrent-ruby-edge/concurrent/channel/selector.rb:52

      def put(channel, message, &block)
        @clauses << PutClause.new(channel, message, block)
      end
      alias_method :send, :put

      def after(seconds, &block)
        @clauses << AfterClause.new(seconds, block)
      end
      alias_method :timeout, :after

      def default(&block)
        raise ArgumentError.new('no block given') unless block_given?
        @clauses << DefaultClause.new(block)
      end

      def error(&block)
        raise ArgumentError.new('no block given') unless block_given?
        raise ArgumentError.new('only one error handler allowed') if @error_handler
        @error_handler = block
      end

      def execute
        raise Channel::Error.new('no clauses given') if @clauses.empty?
        loop do
          done = @clauses.each do |clause|
            result = clause.execute
            break result if result.just?
          end
          break done.value if done.is_a?(Concurrent::Maybe)
          Thread.pass
        end
      rescue => ex
        if @error_handler
          @error_handler.call(ex)
        else
          raise ex

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Keep exactly one sel.error per select and compose behavior inside it: sel.error { |e| log(e); report(e) if e.respond_to?(:critical?) && e.critical? }.
  2. If a shared helper registers error handling, let call sites pass their extra handler into the helper rather than calling sel.error again.
  3. Search your select-building code paths for all sel.error call sites before combining helpers.

Example fix

# before
sel.error { |e| log(e) }
sel.error { |e| report(e) }   # raises: only one error handler allowed

# after
sel.error { |e|
  log(e)
  report(e)
}
Defensive patterns

Strategy: validation

Validate before calling

def build_selector(handlers = {})
  Concurrent::Channel.select do |sel|
    yield sel
    sel.error(&handlers.fetch(:on_error, DEFAULT_ERROR_HANDLER)) unless sel.instance_variable_get(:@error_handler)
  end
end

Prevention

When it happens

Trigger: Calling sel.error twice within one Channel.select block - most often a shared 'register common clauses' helper that adds a standard error handler combined with an inline sel.error at the call site.

Common situations: DSL helper methods that install default error handling (sel.error { log(e) }) plus per-use-site handlers; copy-pasting a select template that already contains sel.error onto one that gets another via a wrapper.

Related errors


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