javan/whenever · error · ArgumentError

#{must_be_between}, #{at.max} given

Error message

#{must_be_between}, #{at.max} given

What it means

Whenever validates the :at option for numeric frequencies against the cron field it controls. When :at is a Ruby Range whose end (at.max) falls outside the field's legal values, range_or_integer raises ArgumentError naming the field, its bounds, and the offending max. Ruby Range ends are inclusive, so (0..24) really does include 24 — one past cron's hour ceiling.

Source

Thrown at lib/whenever/cron.rb:157

        timing = Array.new(4, '*')
        timing[0] = @at.is_a?(Time) ? @at.min  : 0
        timing[1] = @at.is_a?(Time) ? @at.hour : 0

        return (timing << '1-5') * " " if string.downcase.index('weekday')
        return (timing << '6,0') * " " if string.downcase.index('weekend')

        DAYS.each_with_index do |day, i|
          return (timing << i) * " " if string.downcase.index(day)
        end

        raise ArgumentError, "Couldn't parse: #{@time.inspect}"
      end

      def range_or_integer(at, valid_range, name)
        must_be_between = "#{name} must be between #{valid_range.min}-#{valid_range.max}"
        if at.is_a?(Range)
          raise ArgumentError, "#{must_be_between}, #{at.min} given" unless valid_range.include?(at.min)
          raise ArgumentError, "#{must_be_between}, #{at.max} given" unless valid_range.include?(at.max)
          return "#{at.min}-#{at.max}"
        end
        raise ArgumentError, "#{must_be_between}, #{at} given" unless valid_range.include?(at)
        at
      end

      def comma_separated_timing(frequency, max, start = 0)
        return start     if frequency.nil? || frequency == "" || frequency.zero?
        return '*'       if frequency == 1
        return frequency if frequency > (max * 0.5).ceil

        original_start = start

        start += frequency unless (max + 1).modulo(frequency).zero? || start > 0
        output = (start..max).step(frequency).to_a

        max_occurances = (max.to_f  / (frequency.to_f)).round
        max_occurances += 1 if original_start.zero?

View on GitHub (pinned to 756163ed1a)

Solutions

  1. Cap the Range end at the field maximum: hours (0..23), minutes (0..59), days (1..31), months (1..12).
  2. For a time span crossing the boundary (e.g. 22:00 to midnight), use a Chronic string or raw cron instead of a numeric Range.
  3. Add a schedule-generation smoke test (run `whenever` with no flags) to CI so out-of-bounds ends are caught before deploy.

Example fix

# before (24:00 is not a valid cron hour)
every 1.day, at: (9..24) do
  rake 'nightly:job'
end

# after
every 1.day, at: (9..23) do
  rake 'nightly:job'
end
Defensive patterns

Strategy: validation

Validate before calling

LIMITS = { minute: 0..59, hour: 0..23, day: 1..31, month: 1..12 }
# before: every 1.day, at: (9..24)
at = (9..24); field = :hour
unless LIMITS[field].cover?(at.max)
  abort(field.to_s + ' range must end within ' + LIMITS[field].to_s + ' (cron has no 24:00)')
end

Type guard

def valid_at_range?(at, field)
  limits = { minute: 0..59, hour: 0..23, day: 1..31, month: 1..12 }
  at.is_a?(Range) && limits[field].cover?(at.min) && limits[field].cover?(at.max)
end

Try / catch

begin
  Whenever.cron(file: 'config/schedule.rb')
rescue ArgumentError => e
  abort('invalid :at range end in schedule: ' + e.message)
end

Prevention

When it happens

Trigger: every 1.day, at: (9..24) → Hour must be between 0-23, 24 given (24:00 does not exist in cron; use 0 for midnight). every 1.hour, at: (0..60) → Minute must be between 0-59, 60 given. every 1.year, at: (1..13) → Month must be between 1-12, 13 given.

Common situations: Treating 24 as 'end of day' when cron wraps 24 back to 0; half-open interval habits from other languages producing inclusive ends one past the limit; generated schedules whose upper bound is computed rather than hardcoded.

Related errors


AI-assisted analysis of javan/whenever@756163ed1a (2026-08-21). Data as JSON: /api/errors/093faebb0be60a1b. Report an issue: GitHub.