Croner 11.0.0 is a major release focused on correctness and predictability: DST handling aligned with croner-rust and Vixie cron, a new iterator API, and a long list of scheduling fixes around timezones, clock changes and edge-case patterns.
Highlights
-
Iterator API — The new
enumerate()method returns a statefulCronIteratorthat works withfor...of, destructuring,next(),peek()andreset(), and honorsmaxRuns. -
Predictable DST handling — DST transitions now work as in croner-rust and Vixie cron. Fixed-time jobs in a spring-forward gap run at the first valid time after it, and interval jobs keep running through the repeated hour at fall back. Negative timer re-arming during fall back is fixed.
-
Inclusive
startAt— A pattern match exactly atstartAtnow runs. -
dayOffsetthat matches reality —nextRun(),nextRuns(),previousRuns(),msToNext()andenumerate()return actual run times, the same ones the scheduler uses. -
Date-based jobs for "now" — A one-off job dated up to 1 second in the past fires immediately. The new
allowPastoption lets older dates fire too. -
Faster timezone scheduling —
Intl.DateTimeFormatinstances are cached per timezone. -
Timezone and clock robustness — ISO 8601 strings are read in the job's
timezone/utcOffset, and scheduling recovers properly after system clock steps forward or backward.
Breaking Changes
- DST handling aligned with croner-rust and Vixie cron, deliberately deviating from OCPS 1.4 §4.3.1 (
0 30 2 * * *in a DST gap now runs at 3:00 AM instead of 3:30 AM, and interval jobs run on both occurrences of a repeated time) startAtis inclusive (not whenintervalis set)- With
dayOffset,nextRun()and related methods return actual run times, andstartAt/stopAtapply to the shifted run time previousRun()returns when the previous run started, as documented, instead of when it finished
See the migration guide further down for a smooth upgrade from 10.x.
Thank you to all contributors, and a special welcome to @SynthLuvr, @exceptdev, @EmersonBraun, @premsreelathasugeendran, @bensynapse, @youdie006, @sarmah-rup, @Shaisolaris and @cristianchiriac for their first contributions! 🎉
Details
Breaking changes
- DST handling aligned with croner-rust and Vixie cron, deviating from OCPS 1.4 §4.3.1: jobs are either fixed-time jobs (one value each for seconds, minutes and hours) or interval jobs.
- A fixed-time job in a DST gap (spring forward) runs at the first valid time after the gap. In 10.x it ran as much later as the gap was long (
0 30 2 * * *ran at 3:30 AM, now 3:00 AM). - An interval job runs on both occurrences of a time repeated by a DST overlap (fall back), so
* * * * *keeps running through the repeated hour (#343, #407). Patterns with more than one hour, minute or second, like0 30 1,2 * * *, are interval jobs. - Fixed-time jobs still run once, at the first occurrence, in a DST overlap.
- A fixed-time job in a DST gap (spring forward) runs at the first valid time after the gap. In 10.x it ran as much later as the gap was long (
startAtis inclusive: a pattern match exactly atstartAtnow runs. Previously the first run was the following match. This does not apply withintervalset, which works as in 10.x. AstartAtbetween two whole seconds counts from the next whole second, and a run in the same second before a fractionalstopAt(00:00:00 withstopAt00:00:00.500) now runs. 10.x excluded it.- With
dayOffset,nextRun(),nextRuns(),previousRuns(),msToNext()andenumerate()return actual run times, searched from the reference, the same way the scheduler andisRunning()do. Previously they shifted the next pattern match after the reference, so a negative offset could return a time before the reference, andmsToNext()ignored the offset.startAtandstopAtalso apply to the shifted run time. In 10.x they applied to the unshifted pattern match, so a job could run beforestartAtor afterstopAt. previousRun()returns when the previous run started, as documented. Previously it returned when the run finished.
Added
enumerate(startAt?)method returning a statefulCronIteratorthat supports iterator/iterable protocols (for...of, destructuring,next(),peek(),reset()) and honorsmaxRuns(#365). TheCronIteratorclass is exported.allowPastoption for explicitly allowing date-based jobs scheduled more than 1 second in the past to fire immediately (#349).CatchCallbackFn,CronModeandProtectCallbackFnare exported from the source entry point (JSR). The npm type declarations already exposed them.
Changed
- Cached
Intl.DateTimeFormatinstances per timezone, making timezone-aware scheduling noticeably faster (based on #389). - Date-based jobs dated up to 1 second in the past now fire immediately, so a job created for "now" is not lost to the time it took to create it. Older dates fire only with
allowPast: true. Previously a date-based job in the past never fired (#348, #349). - Clarified
dayOffsetJSDoc and completed missing option documentation, including iterator usage,allowPast, and DST behavior. - Updated the copyright year to 2026 (#364).
Fixed
- Timers being re-armed with negative delays throughout the repeated hour of a DST fall back (#399).
nextRun()andmsToNext()no longer return a time in the past when the reference is inside the repeated hour, and searches move in real time in both directions. nextRuns()going backwards or repeating times around a DST spring forward.- DST handling in timezones with a 30-minute overlap (Australia/Lord_Howe) (#404).
msToNext()returning a wrong (often negative) value when passed an ISO 8601 string on a job withtimezoneset. The string was read in system local time (#401, #402).- ISO 8601 strings being read in system local time on jobs with
utcOffset, innextRun(),msToNext(),match(),startAt,stopAtand one-off dates. Strings withZor an offset, like2026-01-01T00:00:00Z, are read as that exact instant (#401). startAtgiven as aDatebeing searched in system local time on jobs withutcOffset.previousRuns()throwing for patterns with month-specific wildcards (e.g.0 9 * 4 *) when the reference date falls outside allowed months, and for leap-day patterns (0 0 29 2 *) in leap years (#408, #409).- Patterns that can never match:
previousRuns()threw aTypeErrorfor patterns like0 0 30 2 *, andnextRun(),nextRuns()andpreviousRuns()threwMaximum call stack size exceededfor0 0 16 * 5#LwithdomAndDow: true. They now returnnulland an empty list. - Scheduling after system clock changes: occurrences silently skipped after forward clock steps (#388, #393), timer and target disagreeing after backward clock steps (#417), and timer targets for
dayOffsetschedules and protected jobs (#395). isRunning(),nextRun()andmsToNext()disagreeing with the scheduler whendayOffsetis set.TimeoutNegativeWarningwhen a timer delay was negative. Negative delays are now clamped to 0 (#370, #369).?in day-of-month or day-of-week behaving differently from*:0 0 ? * MONran every day instead of every Monday (#392).nextRun(),nextRuns(),msToNext()and scheduling throwing aTypeErrorwhen a schedule withstartAtandintervalhas no runs left (#390).RangeErrorwhen using stepping with year field wildcard.*/2in the year field matches even years (#373, #374).- The
@deprecatedJSDoc tag being ondayOffsetinstead oflegacyMode. - ISO 8601 strings losing their milliseconds on jobs without
timezone: a one-off date like2030-01-01T00:00:00.700Zran at.000, and strings andDates could differ instopAt, one-off dates,nextRun()andmsToNext(). Also fixed a one-off date given as a string, later in the same second asstartAt, never running. Jobs with a namedtimezonestill work in whole seconds, as in 10.x (#424).
Documentation
- Migration guide for the 10.x to 11.x upgrade path, including DST behavior.
- Documented the deliberate deviation from OCPS 1.4 §4.3.1 for DST handling.
- Fixed documentation examples that threw or were invalid, and added
@rebootto the pattern docs (#412, #423). - Fixed typos in source comments (#379).
CI / Build
- Development is now Node.js-first:
package.jsonis committed and is the source of truth for version, scripts and dev dependencies, the build script runs on Node.js (npm run build), and tests run withnpm test. Deno is still supported (deno task test, fmt/lint, JSR publishing), andnpm versionkeepsdeno.jsonin sync. - Switched declaration bundling from esbuild to rollup with rollup-plugin-dts, restoring dist parity with previous releases.
- npm publishing uses trusted publishing (OIDC) with provenance.
Upgrading from 10.x to 11.x
Version 11.x adds iterator-based schedule traversal (enumerate()), aligns DST handling with croner-rust, and tightens startAt and dayOffset behavior.
Breaking changes:
startAtis inclusive. A pattern match exactly atstartAtnow runs. In10.x, the first run was the following match. This does not apply withintervalset, which works as in10.x.- With
dayOffset,nextRun()and friends return actual run times. In10.xthey shifted the next pattern match after the reference, sodayOffset: -1could return a time before the reference, andmsToNext()ignored the offset.startAtandstopAtnow also apply to the shifted run time. previousRun()returns when the previous run started, as documented. In10.xit returned when the run finished.
DST behavior (aligned with croner-rust and Vixie cron):
- Fixed-time jobs (one value each for seconds, minutes and hours) in a DST gap run at the first valid time after the gap:
0 30 2 * * *now runs at 3:00 AM instead of 3:30 AM. In a DST overlap they still run once, at the first occurrence. - Interval jobs (anything else) run on both occurrences of a repeated time at fall back, so
* * * * *keeps running through the repeated hour, and skip times that don't exist at spring forward. - Patterns with more than one hour, minute or second, like
0 30 1,2 * * *, are interval jobs.
Other behavior changes:
- ISO 8601 strings are read in the job's
timezoneorutcOffset, inmsToNext(),nextRun(),match(),startAt,stopAtand one-off dates. Strings withZor an offset are read as that exact instant. - Patterns that can never match, like
0 0 30 2 *, makenextRun()returnnullandnextRuns()/previousRuns()return[]instead of throwing.
Past date-based jobs:
- A date-based job dated up to 1 second in the past now fires immediately. To also fire older dates, use
allowPast:new Cron("2020-01-01T00:00:00Z", { allowPast: true }, () => { ... }).
New API:
enumerate(startAt?)returns a statefulCronIterator. It works withfor...ofand destructuring, supportsnext(),peek()andreset(), yields runs strictly after the given start date, and honorsmaxRuns.allowPastoption.- New type exports:
CatchCallbackFn,CronModeandProtectCallbackFn.
See the full migration guide for details.
Full Changelog: 10.0.1...11.0.0