ruby-concurrency/concurrent-ruby · error · ArgumentError

bad options #{options}

Error message

bad options #{options}

What it means

`demonitor(reference, *options)` on an ErlangActor Environment takes its options as bare positional symbols; only `:info` and `:flush` are recognized, removed from the options array via `Array#delete`. After stripping those two symbols, anything left triggers `bad options #{options}` — and the message prints the leftovers verbatim. A keyword-style Hash is the classic trap: `options.delete :info` removes the symbol `:info`, never the element `{info: true}`, so the whole hash survives and is reported.

Source

Thrown at lib/concurrent-ruby-edge/concurrent/edge/erlang_actor.rb:815

        #                 drain signals including the Monitor
        reference              = Reference.new
        @Monitoring[reference] = pid
        if pid.terminated.resolved?
          # always return no-proc when terminated
          tell DownSignal.new(pid, reference, NoActor.new(pid))
        else
          # otherwise let it race
          pid.tell Monitor.new(@Pid, reference)
          # no race, it cannot get anything else than NoActor
          tell DownSignal.new(pid, reference, NoActor.new(pid)) if pid.terminated.resolved?
        end
        reference
      end

      def demonitor(reference, *options)
        info  = options.delete :info
        flush = options.delete :flush
        raise ArgumentError, "bad options #{options}" unless options.empty?

        pid          = @Monitoring.delete reference
        demonitoring = !!pid
        pid.tell DeMonitor.new @Pid, reference if demonitoring

        if flush
          # remove (one) down message having reference from mailbox
          flushed = demonitoring ? !!@Mailbox.try_pop_matching(And[DownSignal, -> m { m.reference == reference }]) : false
          return info ? !flushed : true
        end

        if info
          return false unless demonitoring

          if @Mailbox.peek_matching(And[DownSignal, -> m { m.reference == reference }])
            @MonitoringLateDelivery[reference] = pid # allow to deliver the message once
            return false
          end

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Pass supported flags as plain symbols: `env.demonitor(ref, :info, :flush)`.
  2. Never use `key: value` syntax — this API is symbol-flags only.
  3. Read the leftover options in the message text to identify exactly which argument was rejected, then remove it.
  4. Centralize demonitor calls in one helper that whitelists `[:info, :flush]`.

Example fix

// before
env.demonitor(ref, info: true, flush: true) # bad options {:info=>true, :flush=>true}

// after
env.demonitor(ref, :info, :flush)
Defensive patterns

Strategy: validation

Validate before calling

ALLOWED_DEMONITOR_FLAGS = %i[info flush].freeze

def demonitor!(env, ref, *flags)
  bad = flags - ALLOWED_DEMONITOR_FLAGS
  raise ArgumentError, "unsupported flags: #{bad.inspect}" unless bad.empty?
  env.demonitor(ref, *flags)
end

Try / catch

begin
  env.demonitor(ref, *flags)
rescue ArgumentError => e
  raise unless e.message.start_with?('bad options')
  env.demonitor(ref) # fall back to no flags
end

Prevention

When it happens

Trigger: `env.demonitor(ref, info: true)` -> raises with `bad options {:info=>true}`. `env.demonitor(ref, :info, :async)` -> `:async` is unknown. Correct calls: `env.demonitor(ref)`, `env.demonitor(ref, :info)`, `env.demonitor(ref, :info, :flush)`.

Common situations: Muscle memory from keyword-argument Ruby APIs; porting Erlang's `erlang:demonitor(Ref, [{flush, true}])` options literally as `flush: true`; copy-pasting between monitor/demonitor wrappers where the flag vocabulary drifted.

Related errors


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