ruby-concurrency/concurrent-ruby · error · ArgumentError
seconds must be greater than zero
Error message
seconds must be greater than zero
What it means
ScheduledTask's delay is a relative offset in seconds, converted with #to_f and rejected when negative because a negative delay means the target time is already in the past. Note the mismatch between message and code: the check is delay.to_f < 0.0, so zero (and values like nil or 'abc' whose to_f is 0.0) is silently accepted and runs immediately. Passing an absolute Time object also slips through, since Time#to_f becomes a huge positive epoch offset.
Source
Thrown at lib/concurrent-ruby/concurrent/scheduled_task.rb:180
# @!visibility private
attr_reader :executor
# Schedule a task for execution at a specified future time.
#
# @param [Float] delay the number of seconds to wait for before executing the task
#
# @yield the task to be performed
#
# @!macro executor_and_deref_options
#
# @option opts [object, Array] :args zero or more arguments to be passed the task
# block on execution
#
# @raise [ArgumentError] When no block is given
# @raise [ArgumentError] When given a time that is in the past
def initialize(delay, opts = {}, &task)
raise ArgumentError.new('no block given') unless block_given?
raise ArgumentError.new('seconds must be greater than zero') if delay.to_f < 0.0
super(NULL, opts, &nil)
synchronize do
ns_set_state(:unscheduled)
@parent = opts.fetch(:timer_set, Concurrent.global_timer_set)
@args = get_arguments_from(opts)
@delay = delay.to_f
@task = task
@time = nil
@executor = Options.executor_from_options(opts) || Concurrent.global_io_executor
self.observers = Collection::CopyOnNotifyObserverSet.new
end
end
# The `delay` value given at instantiation.
#
# @return [Float] the initial delay.View on GitHub (pinned to 0b88d5ff75)
Solutions
- Clamp computed delays at the call site: delay = [run_at.to_f - Time.now.to_f, 0].max
- Treat 'deadline already passed' as a business decision: if run_at <= Time.now, run the work synchronously or skip it, then pass the clamped delay
- Reject nil explicitly before calling new if an immediate fire is not intended: raise ArgumentError, 'delay missing' if delay.nil?
- Never pass a Time object; convert it to a relative offset first
Example fix
# before (raises when deadline has passed)
task = Concurrent::ScheduledTask.new(deadline - Time.now) { fire_alarm }
# after (past deadlines run immediately)
delay = [[deadline - Time.now, 0].max, 0].max
task = Concurrent::ScheduledTask.new(delay) { fire_alarm } Defensive patterns
Strategy: validation
Validate before calling
delay = delay.to_f
delay = 0.0 if delay < 0 # past deadlines fire immediately
raise ArgumentError, 'delay required' if delay.nil?
Concurrent::ScheduledTask.new(delay) { work } Type guard
def valid_delay?(v) v.is_a?(Numeric) && !v.nil? && v.to_f >= 0 end
Try / catch
begin
task = Concurrent::ScheduledTask.new(delay) { work }
rescue ArgumentError => e
raise if e.message != 'seconds must be greater than zero'
delay = 0
task = Concurrent::ScheduledTask.new(delay) { work } # deadline already passed: run now
end Prevention
- Never compute delays as raw timestamp differences without clamping to 0
- Reject nil delay explicitly if immediate execution is not intended (nil.to_f passes the check)
- Log schedule_time after creation to sanity-check the computed offset in production
When it happens
Trigger: Computing the delay as run_at - Time.now where run_at is already past; passing '-30' (a negative numeric string); passing nil (nil.to_f == 0.0 passes, task fires immediately); passing a Time object instead of a seconds offset.
Common situations: Scheduling jobs at absolute timestamps taken from a DB or queue; clock skew between the app server and the system that produced the timestamp; DST transitions making a computed offset negative; config values that are absent and become nil.
Related errors
- no block given
- no block given
- Not all dependencies are IVars. Dependencies: #{ inputs.insp
- no block given
- number of threads must be greater than zero
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/27810eabf9dd11ee.
Report an issue: GitHub.