npm croner 11.0.0

3 hours ago

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 stateful CronIterator that works with for...of, destructuring, next(), peek() and reset(), and honors maxRuns.

  • 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 at startAt now runs.

  • dayOffset that matches reality — nextRun(), nextRuns(), previousRuns(), msToNext() and enumerate() 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 allowPast option lets older dates fire too.

  • Faster timezone scheduling — Intl.DateTimeFormat instances 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)
  • startAt is inclusive (not when interval is set)
  • With dayOffset, nextRun() and related methods return actual run times, and startAt/stopAt apply 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, like 0 30 1,2 * * *, are interval jobs.
    • Fixed-time jobs still run once, at the first occurrence, in a DST overlap.
  • startAt is inclusive: a pattern match exactly at startAt now runs. Previously the first run was the following match. This does not apply with interval set, which works as in 10.x. A startAt between two whole seconds counts from the next whole second, and a run in the same second before a fractional stopAt (00:00:00 with stopAt 00:00:00.500) now runs. 10.x excluded it.
  • With dayOffset, nextRun(), nextRuns(), previousRuns(), msToNext() and enumerate() return actual run times, searched from the reference, the same way the scheduler and isRunning() do. Previously they shifted the next pattern match after the reference, so a negative offset could return a time before the reference, and msToNext() ignored the offset. startAt and stopAt also apply to the shifted run time. In 10.x they applied to the unshifted pattern match, so a job could run before startAt or after stopAt.
  • previousRun() returns when the previous run started, as documented. Previously it returned when the run finished.

Added

  • enumerate(startAt?) method returning a stateful CronIterator that supports iterator/iterable protocols (for...of, destructuring, next(), peek(), reset()) and honors maxRuns (#365). The CronIterator class is exported.
  • allowPast option for explicitly allowing date-based jobs scheduled more than 1 second in the past to fire immediately (#349).
  • CatchCallbackFn, CronMode and ProtectCallbackFn are exported from the source entry point (JSR). The npm type declarations already exposed them.

Changed

  • Cached Intl.DateTimeFormat instances 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 dayOffset JSDoc 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() and msToNext() 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 with timezone set. The string was read in system local time (#401, #402).
  • ISO 8601 strings being read in system local time on jobs with utcOffset, in nextRun(), msToNext(), match(), startAt, stopAt and one-off dates. Strings with Z or an offset, like 2026-01-01T00:00:00Z, are read as that exact instant (#401).
  • startAt given as a Date being searched in system local time on jobs with utcOffset.
  • 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 a TypeError for patterns like 0 0 30 2 *, and nextRun(), nextRuns() and previousRuns() threw Maximum call stack size exceeded for 0 0 16 * 5#L with domAndDow: true. They now return null and 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 dayOffset schedules and protected jobs (#395).
  • isRunning(), nextRun() and msToNext() disagreeing with the scheduler when dayOffset is set.
  • TimeoutNegativeWarning when 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 ? * MON ran every day instead of every Monday (#392).
  • nextRun(), nextRuns(), msToNext() and scheduling throwing a TypeError when a schedule with startAt and interval has no runs left (#390).
  • RangeError when using stepping with year field wildcard. */2 in the year field matches even years (#373, #374).
  • The @deprecated JSDoc tag being on dayOffset instead of legacyMode.
  • ISO 8601 strings losing their milliseconds on jobs without timezone: a one-off date like 2030-01-01T00:00:00.700Z ran at .000, and strings and Dates could differ in stopAt, one-off dates, nextRun() and msToNext(). Also fixed a one-off date given as a string, later in the same second as startAt, never running. Jobs with a named timezone still 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 @reboot to the pattern docs (#412, #423).
  • Fixed typos in source comments (#379).

CI / Build

  • Development is now Node.js-first: package.json is 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 with npm test. Deno is still supported (deno task test, fmt/lint, JSR publishing), and npm version keeps deno.json in 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:

  • startAt is inclusive. A pattern match exactly at startAt now runs. In 10.x, the first run was the following match. This does not apply with interval set, which works as in 10.x.
  • With dayOffset, nextRun() and friends return actual run times. In 10.x they shifted the next pattern match after the reference, so dayOffset: -1 could return a time before the reference, and msToNext() ignored the offset. startAt and stopAt now also apply to the shifted run time.
  • previousRun() returns when the previous run started, as documented. In 10.x it 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 timezone or utcOffset, in msToNext(), nextRun(), match(), startAt, stopAt and one-off dates. Strings with Z or an offset are read as that exact instant.
  • Patterns that can never match, like 0 0 30 2 *, make nextRun() return null and nextRuns() / 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 stateful CronIterator. It works with for...of and destructuring, supports next(), peek() and reset(), yields runs strictly after the given start date, and honors maxRuns.
  • allowPast option.
  • New type exports: CatchCallbackFn, CronMode and ProtectCallbackFn.

See the full migration guide for details.

Full Changelog: 10.0.1...11.0.0

Don't miss a new croner release

NewReleases is sending notifications on new releases.