ruby-concurrency/concurrent-ruby · error · ArgumentError

no action given

Error message

no action given

What it means

Every Agent action is dispatched through enqueue_action_job, which requires a non-nil action - a proc/lambda/method object, or the block that Agent#send converts for you. A nil action means there is nothing to execute, so it raises ArgumentError before the job is enqueued.

Source

Thrown at lib/concurrent-ruby/concurrent/agent.rb:511

      if @error_mode && !ERROR_MODES.include?(@error_mode)
        raise ArgumentError.new('unrecognized error mode')
      elsif @error_mode.nil?
        @error_mode = @error_handler ? :continue : :fail
      end

      @error_handler ||= DEFAULT_ERROR_HANDLER
      @validator     = opts.fetch(:validator, DEFAULT_VALIDATOR)
      @current       = Concurrent::AtomicReference.new(initial)
      @error         = Concurrent::AtomicReference.new(nil)
      @caller        = Concurrent::ThreadLocalVar.new(nil)
      @queue         = []

      self.observers = Collection::CopyOnNotifyObserverSet.new
    end

    def enqueue_action_job(action, args, executor)
      raise ArgumentError.new('no action given') unless action
      job = Job.new(action, args, executor, @caller.value || Thread.current.object_id)
      synchronize { ns_enqueue_job(job) }
    end

    def enqueue_await_job(latch)
      synchronize do
        if (index = ns_find_last_job_for_thread)
          job = Job.new(AWAIT_ACTION, [latch], Concurrent.global_immediate_executor,
                        Thread.current.object_id)
          ns_enqueue_job(job, index+1)
        else
          latch.count_down
          true
        end
      end
    end

    def ns_enqueue_job(job, index = nil)

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Check the action before dispatching: agent.send(action) if action.
  2. Fail loudly at lookup time: action = handlers.fetch(step) { raise KeyError, "no handler for #{step}" } instead of silently sending nil.
  3. When there is no named action, use the block form: agent.send { |old_value| compute(old_value) }.

Example fix

# before
agent.send(handlers[step])   # handlers[step] is nil for unknown steps

# after
action = handlers.fetch(step) { raise KeyError, "no handler for #{step}" }
agent.send(action)
Defensive patterns

Strategy: validation

Validate before calling

raise ArgumentError, "no action for #{step.inspect}" if action.nil?
agent.send(action) if action

Try / catch

begin
  agent.send(action, *args)
rescue ArgumentError => e
  raise ConfigError, "agent dispatch failed: #{e.message}"
end

Prevention

When it happens

Trigger: agent.send(nil); agent.send(handlers[step]) where the lookup returned nil for an unknown step; dynamically built actions (strategy maps, 'do_#{step}'.to_sym lookups, conditional procs like cond ? proc {...} : nil) dispatched without a nil check.

Common situations: Actions built from a lookup table or strategy hash with missing keys; passing opts[:handler] that was never set; conditional action construction where one branch returns nil but dispatch happens unconditionally.

Related errors


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