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).

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)

Solutions

  1. Clamp the value before registering: sel.after([remaining, 0.0].max).
  2. Treat negative remaining as 'already timed out' and run the timeout path immediately (or register sel.default) instead of calling after.
  3. 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

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

Related errors


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