flowable/flowable-engine · error · java.lang.IllegalArgumentException
Start year must be less than stop year
Error message
Start year must be less than stop year
What it means
Inside addToSet, when a range's end is before its start (e.g. 22-2 hours), CronExpression overflows into the next cycle by adding a per-field maximum (60, 24, 31, ...). YEAR has no cyclic maximum, so a year range whose end is less than its start (e.g. '2026-2020') hits the switch's YEAR case and throws an IllegalArgumentException (unchecked, NOT ParseException) with this message.
Solutions
- Swap the years so the range is ascending, e.g. 2025-2030 instead of 2030-2025.
- If a descending range is intentional, expand it into a list or two separate expressions/cron schedules.
- Catch IllegalArgumentException (not ParseException) when constructing CronExpression with user-supplied year fields, and validate start <= stop before formatting the string.
- Simplify: drop the year field entirely (Flowable timers rarely need it) so the 6-field expression has no year range to invert.
Example fix
// before
CronExpression expr = new CronExpression("0 0 0 1 1 ? 2030-2025"); // throws
// after
CronExpression expr = new CronExpression("0 0 0 1 1 ? 2025-2030"); Defensive patterns
Strategy: validation
Validate before calling
if (startYear > endYear) {
throw new IllegalArgumentException("Year range must be ascending: " + startYear + "-" + endYear);
}
String cron = "0 0 0 1 1 ? " + startYear + "-" + endYear; Try / catch
try {
CronExpression expr = new CronExpression(cron);
} catch (IllegalArgumentException e) {
// year range start >= stop (note: unchecked, distinct from ParseException)
throw new ConfigurationException("Invalid year range in cron: " + cron, e);
} catch (ParseException e) {
throw new ConfigurationException("Invalid cron: " + cron, e);
} Prevention
- Never emit descending year ranges; assert start <= stop when templating
- Remember only YEAR lacks wrap-around ranges; other fields may overflow (e.g. 22-2 hours)
- Catch IllegalArgumentException separately from ParseException when parsing user cron
- Omit the year field unless you truly need it
When it happens
Trigger: new CronExpression(cronString) whose year field (7th, optional) contains a descending range like '0 0 0 1 1 ? 2030-2025'. Only the YEAR field triggers this; descending ranges in other fields are legal wrap-around ranges.
Common situations: Dynamically assembled year ranges where start/stop were swapped; template placeholders replaced out of order; assuming all fields support wrap-around ranges like hours do.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Day of month values must be between 1 and 31
- Hour values must be between 0 and 23
- Minute and Second values must be between 0 and 59
- Month values must be between 1 and 12
- '#' option is not valid here. (pos=
AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11).
Data as JSON: /api/errors/b437762a93b42c39.
Report an issue: GitHub.
Appendix: source
Thrown at modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/calendar/CronExpression.java:1073
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) {
max = switch (type) {
case SECOND -> 60;
case MINUTE -> 60;
case HOUR -> 24;
case MONTH -> 12;
case DAY_OF_WEEK -> 7;
case DAY_OF_MONTH -> 31;
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 d6d39ce1c6)