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
endView on GitHub (pinned to 0b88d5ff75)
Solutions
- Pass supported flags as plain symbols: `env.demonitor(ref, :info, :flush)`.
- Never use `key: value` syntax — this API is symbol-flags only.
- Read the leftover options in the message text to identify exactly which argument was rejected, then remove it.
- 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
- This API takes bare symbols only: :info, :flush.
- Never pass key: value hashes — Array#delete :info cannot remove them.
- Read the leftover options printed in the message to find the culprit argument.
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
- :class option is ignored when calling on context class, use
- environment has to be a class inheriting from Environment or
- only one of block or value can be supplied
- probe is not Resolvable
- unrecognized error mode
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/efed7fbd8cf5131c.
Report an issue: GitHub.