xuxueli/xxl-job · error · IllegalArgumentException

Start year must be less than stop year

Error message

Start year must be less than stop year

What it means

Thrown as IllegalArgumentException inside the range-overflow handler of CronExpression. When a range's stop value is below its start value, the parser normally wraps via modulus (e.g. hours 22-2). The YEAR type has no modulus max and cannot wrap, so a reversed year range is rejected outright.

Source

Thrown at xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/cron/CronExpression.java:1090

            }
            if (startAt == -1 || startAt == ALL_SPEC_INT) {
                startAt = 1970;
            }
        }

        // if the end of the range is before the start, then we need to overflow into
        // the next day, month etc. This is done by adding the maximum amount for that
        // type, and using modulus max to determine the value being added.
        int max = -1;
        if (stopAt < startAt) {
            switch (type) {
                case       SECOND : max = 60; break;
                case       MINUTE : max = 60; break;
                case         HOUR : max = 24; break;
                case        MONTH : max = 12; break;
                case  DAY_OF_WEEK : max = 7;  break;
                case DAY_OF_MONTH : max = 31; break;
                case         YEAR : throw new IllegalArgumentException("Start year must be less than stop year");
                default           : throw new IllegalArgumentException("Unexpected type encountered");
            }
            stopAt += max;
        }

        for (int i = startAt; i <= stopAt; i += incr) {
            if (max == -1) {
                // ie: there's no max to overflow over
                set.add(i);
            } else {
                // take the modulus to get the real value
                int i2 = i % max;

                // 1-indexed ranges should not include 0, and should include their max
                if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH) ) {
                    i2 = max;
                }

View on GitHub (pinned to e74c784f68)

Solutions

  1. Order year ranges ascending: start year must be less than stop year (e.g. 2010-2020, not 2020-2010).
  2. For a single year, use a literal rather than a range.
  3. Sort the two endpoints before formatting the YEAR field when generating cron programmatically.

Example fix

// before
new CronExpression("0 0 12 ? * * 2020-2010"); // reversed
// after
new CronExpression("0 0 12 ? * * 2010-2020"); // ascending
Defensive patterns

Strategy: validation

Validate before calling

// Ensure year ranges are ascending before formatting
int startYear = 2010, stopYear = 2020;
if (stopYear < startYear) throw new IllegalArgumentException("start year must be < stop year");
String yearField = startYear + "-" + stopYear;

Try / catch

try {
    CronExpression c = new CronExpression(expr);
} catch (java.text.ParseException | IllegalArgumentException e) {
    log.warn("Invalid cron '{}': {}", expr, e.getMessage());
}

Prevention

When it happens

Trigger: Providing a YEAR field that is a reversed range, e.g. CronExpression("0 0 12 ? * * 2020-2010"). Any stopAt < startAt on the YEAR field triggers it during parsing.

Common situations: Building year ranges dynamically and accidentally swapping start/end; reusing a generic range formatter that sorted endpoints descending; interpreting the range as inclusive bounds without ordering them.

Related errors


AI-assisted analysis of xuxueli/xxl-job@e74c784f68 (2026-08-14). Data as JSON: /api/errors/b0a5304374e4c439. Report an issue: GitHub.