{"record":{"id":"23361e9b7784b6ca","repo":"ruby-concurrency/concurrent-ruby","slug":"only-one-error-handler-allowed","errorCode":null,"errorMessage":"only one error handler allowed","messagePattern":"only one error handler allowed","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/concurrent-ruby-edge/concurrent/channel/selector.rb","lineNumber":52,"sourceCode":"\n      def put(channel, message, &block)\n        @clauses << PutClause.new(channel, message, block)\n      end\n      alias_method :send, :put\n\n      def after(seconds, &block)\n        @clauses << AfterClause.new(seconds, block)\n      end\n      alias_method :timeout, :after\n\n      def default(&block)\n        raise ArgumentError.new('no block given') unless block_given?\n        @clauses << DefaultClause.new(block)\n      end\n\n      def error(&block)\n        raise ArgumentError.new('no block given') unless block_given?\n        raise ArgumentError.new('only one error handler allowed') if @error_handler\n        @error_handler = block\n      end\n\n      def execute\n        raise Channel::Error.new('no clauses given') if @clauses.empty?\n        loop do\n          done = @clauses.each do |clause|\n            result = clause.execute\n            break result if result.just?\n          end\n          break done.value if done.is_a?(Concurrent::Maybe)\n          Thread.pass\n        end\n      rescue => ex\n        if @error_handler\n          @error_handler.call(ex)\n        else\n          raise ex","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/ruby-concurrency/concurrent-ruby/blob/0b88d5ff75f69b3740c8f0868e76f833cb2fd45d/lib/concurrent-ruby-edge/concurrent/channel/selector.rb#L34-L70","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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? }.","If a shared helper registers error handling, let call sites pass their extra handler into the helper rather than calling sel.error again.","Search your select-building code paths for all sel.error call sites before combining helpers."],"exampleFix":"# before\nsel.error { |e| log(e) }\nsel.error { |e| report(e) }   # raises: only one error handler allowed\n\n# after\nsel.error { |e|\n  log(e)\n  report(e)\n}","handlingStrategy":"validation","validationCode":"def build_selector(handlers = {})\n  Concurrent::Channel.select do |sel|\n    yield sel\n    sel.error(&handlers.fetch(:on_error, DEFAULT_ERROR_HANDLER)) unless sel.instance_variable_get(:@error_handler)\n  end\nend","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Exactly one sel.error per select - compose multiple behaviors inside a single block.","If a shared helper installs error handling, do not also call sel.error at call sites; pass extra handlers into the helper instead.","Grep for sel.error across helper + call-site before combining select templates."],"tags":["ruby","concurrent-ruby","selector","error-handling","argumenterror"],"backgroundTag":"duplicate-handler-registration","analyzedSha":"0b88d5ff75f69b3740c8f0868e76f833cb2fd45d","analyzedAt":"2026-08-21T20:12:56.291Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}