ruby-concurrency/concurrent-ruby · error · ArgumentError
timeout must 0.0 or more
Error message
timeout must 0.0 or more
What it means
A selector's AfterClause arms a deadline at current monotonic time plus the given seconds. A negative delay would make the clause fire in the past, which the constructor rejects (the message 'timeout must 0.0 or more' is the library's own wording, typo included).
Solutions
- Clamp the value before registering: sel.after([remaining, 0.0].max).
- Treat negative remaining as 'already timed out' and run the timeout path immediately (or register sel.default) instead of calling after.
- Use nil for 'no timeout' rather than a negative sentinel, and check for it explicitly.
Example fix
# before
sel.after(deadline - Concurrent.monotonic_time) # negative once deadline passed
# after
remaining = [deadline - Concurrent.monotonic_time, 0.0].max
sel.after(remaining) { handle_timeout } Defensive patterns
Strategy: validation
Validate before calling
delay = Float(seconds)
sel.after([delay, 0.0].max) { handle_timeout } unless delay.nan? Try / catch
begin
sel.after(delay) { handle_timeout }
rescue ArgumentError
sel.after(0.0) { handle_timeout } # already expired: fire immediately
end Prevention
- Clamp computed remaining-time values before passing them to after/timeout.
- Use nil (checked explicitly) for 'no timeout', never a negative sentinel.
- Zero is valid and means 'already expired' - useful for immediate-fire defaults.
When it happens
Trigger: sel.after(-1) or sel.timeout(-0.5) inside a Channel.select block; computed delays like deadline - Concurrent.monotonic_time that dip below zero once the deadline has already passed.
Common situations: Timeouts derived from remaining time (remaining = total - elapsed) that go negative on retries or slow iterations; config/ENV timeout values that use -1 as a 'disabled' sentinel; arithmetic on user-supplied durations.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- invalid action
- no block given
- only one error handler allowed
- capacity must be at least 1 for this buffer type
- no action given
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/0c916a3d75a0399d.
Report an issue: GitHub.
Appendix: source
Thrown at lib/concurrent-ruby-edge/concurrent/channel/selector/after_clause.rb:11
require 'concurrent/maybe'
require 'concurrent/utility/monotonic_time'
module Concurrent
class Channel
class Selector
class AfterClause
def initialize(seconds, block)
raise ArgumentError.new('timeout must 0.0 or more') if seconds.to_f < 0.0
@end = Concurrent.monotonic_time + seconds.to_f
@block = block
end
def execute
if Concurrent.monotonic_time > @end
result = @block ? @block.call : nil
Concurrent::Maybe.just(result)
else
Concurrent::Maybe.nothing
end
end
end
end
end
end
View on GitHub (pinned to 0b88d5ff75)