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, :afterView on GitHub (pinned to 0b88d5ff75)
Solutions
- Use a supported action - take side: :take, :poll, :receive, :~; put side: :put, :offer, :send, :<<.
- Or call the specific registrar directly: sel.take(ch) { |v| ... } / sel.put(ch, msg) { ... }.
- 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
- Symbols only - symbolize strings from config before they reach sel.case.
- Consider calling sel.take/sel.put directly; the generic #case adds a dispatch layer that invites typos.
- Whitelist-validate DSL inputs at the boundary.
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
- no block given
- unbuffered channels cannot have a capacity
- capacity must be at least 1 for this buffer type
- no block given
- size must be greater than 0
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/705af9b3025f0d16.
Report an issue: GitHub.