ruby-concurrency/concurrent-ruby · error · ArgumentError

invalid action

Error message

invalid action

What it means

Selector#case is the generic clause registrar inside Channel.select: it dispatches on the action symbol to a take clause (:take, :poll, :receive, :~) or a put clause (:put, :offer, :send, :<<). Any other action raises, because no clause type exists for it.

Source

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

module Concurrent
  class Channel

    # @!visibility private
    class Selector

      def initialize
        @clauses = []
        @error_handler = nil
      end

      def case(channel, action, message = nil, &block)
        if [:take, :poll, :receive, :~].include?(action)
          take(channel, &block)
        elsif [:put, :offer, :send, :<<].include?(action)
          put(channel, message, &block)
        else
          raise ArgumentError.new('invalid action')
        end
      end

      def take(channel, &block)
        raise ArgumentError.new('no block given') unless block_given?
        @clauses << TakeClause.new(channel, block)
      end
      alias_method :receive, :take

      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

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Use a supported action - take side: :take, :poll, :receive, :~; put side: :put, :offer, :send, :<<.
  2. Or call the specific registrar directly: sel.take(ch) { |v| ... } / sel.put(ch, msg) { ... }.
  3. When the action comes from data, symbolize (.to_sym) and validate against a whitelist before the select block runs.

Example fix

# before
Concurrent::Channel.select do |sel|
  sel.case(ch, :read) { |v| handle(v) }   # invalid action
end

# after
Concurrent::Channel.select do |sel|
  sel.case(ch, :take) { |v| handle(v) }
end
Defensive patterns

Strategy: type-guard

Type guard

VALID_ACTIONS = %i[take poll receive ~ put offer send <<].freeze

def valid_selector_action?(action)
  VALID_ACTIONS.include?(action)
end

sel.case(ch, action) { |v| handle(v) } if valid_selector_action?(action)

Try / catch

begin
  sel.case(ch, action) { |v| handle(v) }
rescue ArgumentError => e
  raise ArgumentError, "unsupported action #{action.inspect}; use one of #{VALID_ACTIONS}"
end

Prevention

When it happens

Trigger: sel.case(ch, :read), sel.case(ch, :push), sel.case(ch, 'take') (string instead of symbol), or any action from outside the two whitelists.

Common situations: Porting Go or IO idioms and guessing action names (:read/:write/:pop); actions arriving as data from config or a DSL where a string slips through; typos like :recieve or :offre.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


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