{"record":{"id":"b437762a93b42c39","repo":"flowable/flowable-engine","slug":"start-year-must-be-less-than-stop-year","errorCode":null,"errorMessage":"Start year must be less than stop year","messagePattern":"Start year must be less than stop year","errorType":"validation","errorClass":"java.lang.IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/calendar/CronExpression.java","lineNumber":1073,"sourceCode":"                startAt = 1970;\n            }\n        }\n\n        // if the end of the range is before the start, then we need to overflow\n        // into\n        // the next day, month etc. This is done by adding the maximum amount\n        // for that\n        // type, and using modulus max to determine the value being added.\n        int max = -1;\n        if (stopAt < startAt) {\n            max = switch (type) {\n                case SECOND -> 60;\n                case MINUTE -> 60;\n                case HOUR -> 24;\n                case MONTH -> 12;\n                case DAY_OF_WEEK -> 7;\n                case DAY_OF_MONTH -> 31;\n                case YEAR -> throw new IllegalArgumentException(\"Start year must be less than stop year\");\n                default -> throw new IllegalArgumentException(\"Unexpected type encountered\");\n            };\n            stopAt += max;\n        }\n\n        for (int i = startAt; i <= stopAt; i += incr) {\n            if (max == -1) {\n                // ie: there's no max to overflow over\n                set.add(i);\n            } else {\n                // take the modulus to get the real value\n                int i2 = i % max;\n\n                // 1-indexed ranges should not include 0, and should include\n                // their max\n                if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH)) {\n                    i2 = max;\n                }","sourceCodeStart":1055,"sourceCodeEnd":1091,"githubUrl":"https://github.com/flowable/flowable-engine/blob/d6d39ce1c69ff244f2d9dc6af756a9b95e865586/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/calendar/CronExpression.java#L1055-L1091","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nCronExpression expr = new CronExpression(\"0 0 0 1 1 ? 2030-2025\"); // throws\n// after\nCronExpression expr = new CronExpression(\"0 0 0 1 1 ? 2025-2030\");","handlingStrategy":"validation","validationCode":"if (startYear > endYear) {\n    throw new IllegalArgumentException(\"Year range must be ascending: \" + startYear + \"-\" + endYear);\n}\nString cron = \"0 0 0 1 1 ? \" + startYear + \"-\" + endYear;","typeGuard":null,"tryCatchPattern":"try {\n    CronExpression expr = new CronExpression(cron);\n} catch (IllegalArgumentException e) {\n    // year range start >= stop (note: unchecked, distinct from ParseException)\n    throw new ConfigurationException(\"Invalid year range in cron: \" + cron, e);\n} catch (ParseException e) {\n    throw new ConfigurationException(\"Invalid cron: \" + cron, e);\n}","preventionTips":["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"],"tags":["cron","scheduling","validation","year"],"backgroundTag":"invalid-argument-value","analyzedSha":"d6d39ce1c69ff244f2d9dc6af756a9b95e865586","analyzedAt":"2026-09-11T06:41:19.413Z","contentChangedAt":"2026-09-11T06:41:19.413Z","schemaVersion":2},"datasetVersion":"2026-09-18T11:17:12.947Z"}