···11# cron-fast
2233-[](https://www.npmjs.com/package/cron-fast)
44-[](https://www.npmjs.com/package/cron-fast)
33+[](https://www.npmjs.com/package/cron-fast)
44+[](https://www.npmjs.com/package/cron-fast)
55[](https://jsr.io/@kbilkis/cron-fast)
66[](https://jsr.io/@kbilkis/cron-fast)
77[](https://github.com/kbilkis/cron-fast/actions/workflows/ci.yml)
88-[](https://codecov.io/github/kbilkis/cron-fast)
99-[](https://bundlejs.com/?q=cron-fast)
88+[](https://codecov.io/github/kbilkis/cron-fast)
99+[](https://bundlejs.com/?q=cron-fast)
1010[](https://opensource.org/licenses/MIT)
11111212**Fast and tiny JavaScript/TypeScript cron parser with timezone support.** Works everywhere: Node.js, Deno, Bun, Cloudflare Workers, and browsers. Zero dependencies.
···71717272### `nextRun(expression, options?)`
73737474-Get the next execution time for a cron expression.
7474+Get the next execution time for a cron expression. **Throws** if the expression or timezone is invalid.
75757676```typescript
7777nextRun("0 9 * * *"); // Next 9:00 AM UTC
···81818282### `previousRun(expression, options?)`
83838484-Get the previous execution time.
8484+Get the previous execution time. **Throws** if the expression or timezone is invalid.
85858686```typescript
8787previousRun("0 9 * * *"); // Last 9:00 AM UTC
···90909191### `nextRuns(expression, count, options?)`
92929393-Get next N execution times.
9393+Get next N execution times. **Throws** if the expression or timezone is invalid.
94949595```typescript
9696nextRuns("0 9 * * *", 5); // Next 5 occurrences
···107107108108### `isMatch(expression, date, options?)`
109109110110-Check if a date matches the cron expression.
110110+Check if a date matches the cron expression. **Throws** if the expression or timezone is invalid.
111111112112```typescript
113113isMatch("0 9 * * *", new Date("2026-03-15T09:00:00Z")); // true
···230230### Validation and Parsing
231231232232```typescript
233233-// Validate before using
233233+// Functions throw on invalid input, but you can pre-validate user input
234234if (!isValid(userInput)) {
235235- throw new Error("Invalid cron expression");
235235+ console.log("Invalid cron expression");
236236+ return;
237237+}
238238+239239+// Or use try/catch
240240+try {
241241+ const next = nextRun(userInput);
242242+} catch (e) {
243243+ console.log("Invalid cron expression");
236244}
237245238246// Parse to see what it means
···264272265273## Tips & Gotchas
266274275275+- **Invalid input throws**: `nextRun`, `previousRun`, `nextRuns`, and `isMatch` throw an error for invalid cron expressions or invalid timezones. Use `isValid()` to pre-validate user input, or wrap calls in try/catch.
267276- **Timezone handling**: The cron expression is interpreted in the timezone you specify, but the returned Date is always in UTC
268277- **Daylight saving time**: Use IANA timezone names (like "America/New_York") instead of abbreviations (like "EST")
269269-- **Validation**: Always check `isValid()` before parsing user input
270278- **Day 0 and 7**: Both represent Sunday in the day-of-week field
271279- **Ranges are inclusive**: `1-5` includes both 1 and 5
272280
+1-1
jsr.json
···11{
22 "name": "@kbilkis/cron-fast",
33- "version": "1.0.0",
33+ "version": "2.0.0",
44 "description": "Fast and tiny JavaScript/TypeScript cron parser with timezone support - works in Node.js, Deno, Bun, Cloudflare Workers, and browsers. Zero dependencies.",
55 "keywords": [
66 "javascript",
+1-1
package.json
···11{
22 "name": "cron-fast",
33- "version": "1.0.0",
33+ "version": "2.0.0",
44 "description": "Fast and tiny JavaScript/TypeScript cron parser with timezone support - works in Node.js, Deno, Bun, Cloudflare Workers, and browsers. Zero dependencies.",
55 "keywords": [
66 "browser",
+25-20
src/scheduler.ts
···3131 },
3232} as const;
33333434-/** Get the next execution time for a cron expression. Returns null if invalid or no match found. */
3535-export function nextRun(expression: string, options?: CronOptions): Date | null {
3434+/** Get the next execution time for a cron expression. Throws if expression or timezone is invalid, or if no match is found within iteration limit. */
3535+export function nextRun(expression: string, options?: CronOptions): Date {
3636 const parsed = parse(expression);
3737- if (!parsed) return null;
3737+ if (!parsed) throw new Error(`Invalid cron expression: "${expression}"`);
38383939 const from = options?.from || new Date();
4040 const tz = options?.timezone;
41414242 const start = tz !== undefined ? convertToTimezone(from, tz) : new Date(from);
4343- if (!start) return null;
4343+ if (!start) throw new Error(`Invalid timezone: "${tz}"`);
44444545 start.setUTCSeconds(0, 0);
4646 start.setUTCMinutes(start.getUTCMinutes() + 1);
47474848- return findMatch(parsed, start, "next", tz);
4848+ return findMatch(parsed, start, "next", tz, expression);
4949}
50505151-/** Get the previous execution time for a cron expression. Returns null if invalid or no match found. */
5252-export function previousRun(expression: string, options?: CronOptions): Date | null {
5151+/** Get the previous execution time for a cron expression. Throws if expression or timezone is invalid, or if no match is found within iteration limit. */
5252+export function previousRun(expression: string, options?: CronOptions): Date {
5353 const parsed = parse(expression);
5454- if (!parsed) return null;
5454+ if (!parsed) throw new Error(`Invalid cron expression: "${expression}"`);
55555656 const from = options?.from || new Date();
5757 const tz = options?.timezone;
58585959 const start = tz !== undefined ? convertToTimezone(from, tz) : new Date(from);
6060- if (!start) return null;
6060+ if (!start) throw new Error(`Invalid timezone: "${tz}"`);
61616262 start.setUTCSeconds(0, 0);
6363 start.setUTCMinutes(start.getUTCMinutes() - 1);
64646565- return findMatch(parsed, start, "prev", tz);
6565+ return findMatch(parsed, start, "prev", tz, expression);
6666}
67676868-/** Get next N execution times. Returns empty array if invalid. */
6868+/** Get next N execution times. Throws if expression or timezone is invalid. */
6969export function nextRuns(expression: string, count: number, options?: CronOptions): Date[] {
7070 if (count <= 0) return [];
7171···74747575 for (let i = 0; i < count; i++) {
7676 const next = nextRun(expression, { ...options, from: current });
7777- if (!next) break;
7877 results.push(next);
7978 current = new Date(next.getTime() + ONE_MINUTE_MS);
8079 }
8180 return results;
8281}
83828484-/** Check if a date matches the cron expression. Returns false if invalid. */
8383+/** Check if a date matches the cron expression. Throws if expression or timezone is invalid. */
8584export function isMatch(
8685 expression: string,
8786 date: Date,
8887 options?: Pick<CronOptions, "timezone">,
8988): boolean {
9089 const parsed = parse(expression);
9191- if (!parsed) return false;
9090+ if (!parsed) throw new Error(`Invalid cron expression: "${expression}"`);
92919393- const checkDate =
9494- options?.timezone !== undefined ? convertToTimezone(date, options.timezone) : new Date(date);
9292+ const tz = options?.timezone;
9393+ const checkDate = tz !== undefined ? convertToTimezone(date, tz) : new Date(date);
95949696- if (!checkDate) return false;
9595+ if (!checkDate) throw new Error(`Invalid timezone: "${tz}"`);
9796 return matches(parsed, checkDate);
9897}
999810099/** Find matching time using smart field-increment algorithm */
101101-function findMatch(parsed: ParsedCron, start: Date, dir: Direction, tz?: string): Date | null {
100100+function findMatch(
101101+ parsed: ParsedCron,
102102+ start: Date,
103103+ dir: Direction,
104104+ tz?: string,
105105+ expression?: string,
106106+): Date {
102107 const current = new Date(start);
103108104109 for (let i = 0; i < MAX_ITERATIONS; i++) {
105110 if (matches(parsed, current)) {
106106- return tz !== undefined ? convertFromTimezone(current, tz) : current;
111111+ return tz !== undefined ? convertFromTimezone(current, tz)! : current;
107112 }
108113 advanceDate(parsed, current, dir);
109114 }
110110- return null;
115115+ throw new Error(`No match found for "${expression}" within iteration limit`);
111116}
112117113118/**
+101-8
test/scheduler.test.ts
···33import { getDaysInMonth } from "../src/matcher.js";
4455describe("scheduler", () => {
66+ describe("Invalid cron expression handling", () => {
77+ describe("nextRun", () => {
88+ it("should throw for invalid cron expression", () => {
99+ expect(() => nextRun("invalid")).toThrow('Invalid cron expression: "invalid"');
1010+ });
1111+1212+ it("should throw for empty string", () => {
1313+ expect(() => nextRun("")).toThrow('Invalid cron expression: ""');
1414+ });
1515+1616+ it("should throw for wrong number of fields", () => {
1717+ expect(() => nextRun("* * *")).toThrow('Invalid cron expression: "* * *"');
1818+ });
1919+2020+ it("should throw for impossible day/month combination", () => {
2121+ expect(() => nextRun("0 0 31 2 *")).toThrow('Invalid cron expression: "0 0 31 2 *"');
2222+ });
2323+ });
2424+2525+ describe("previousRun", () => {
2626+ it("should throw for invalid cron expression", () => {
2727+ expect(() => previousRun("invalid")).toThrow('Invalid cron expression: "invalid"');
2828+ });
2929+3030+ it("should throw for empty string", () => {
3131+ expect(() => previousRun("")).toThrow('Invalid cron expression: ""');
3232+ });
3333+ });
3434+3535+ describe("nextRuns", () => {
3636+ it("should throw for invalid cron expression", () => {
3737+ expect(() => nextRuns("invalid", 5)).toThrow('Invalid cron expression: "invalid"');
3838+ });
3939+4040+ it("should throw for empty string", () => {
4141+ expect(() => nextRuns("", 3)).toThrow('Invalid cron expression: ""');
4242+ });
4343+ });
4444+4545+ describe("isMatch", () => {
4646+ it("should throw for invalid cron expression", () => {
4747+ const date = new Date("2026-03-15T09:00:00Z");
4848+ expect(() => isMatch("invalid", date)).toThrow('Invalid cron expression: "invalid"');
4949+ });
5050+5151+ it("should throw for empty string", () => {
5252+ const date = new Date("2026-03-15T09:00:00Z");
5353+ expect(() => isMatch("", date)).toThrow('Invalid cron expression: ""');
5454+ });
5555+ });
5656+ });
5757+658 describe("Basic functionality", () => {
759 describe("nextRun", () => {
860 it("should find next run for simple expression", () => {
···550602 // March 2021: DST started March 14, so EDT (UTC-4)
551603 expect(next.getUTCHours()).toBe(14); // 10 AM EDT = 2 PM UTC
552604 expect(next.getTime()).toBeGreaterThan(from.getTime());
605605+ });
606606+ });
607607+608608+ describe("previousRun with seconds in current minute", () => {
609609+ it("should NOT return the current minute when called mid-minute", () => {
610610+ // At 9:01:30, previousRun should return 9:00, not 9:01
611611+ // because we subtract 1 minute after zeroing seconds
612612+ const from = new Date("2025-03-15T09:01:30Z");
613613+ const prev = previousRun("* * * * *", { from });
614614+615615+ expect(prev.getUTCHours()).toBe(9);
616616+ expect(prev.getUTCMinutes()).toBe(0); // 9:00, not 9:01
617617+ expect(prev.getUTCSeconds()).toBe(0);
618618+ expect(prev.getTime()).toBeLessThan(from.getTime());
619619+ });
620620+621621+ it("should return previous minute even at 9:01:00 exactly (start of minute)", () => {
622622+ // At 9:01:00, after zeroing seconds it's still 9:01:00
623623+ // Then subtracting 1 minute gives 9:00:00
624624+ const from = new Date("2025-03-15T09:01:00Z");
625625+ const prev = previousRun("* * * * *", { from });
626626+627627+ expect(prev.getUTCHours()).toBe(9);
628628+ expect(prev.getUTCMinutes()).toBe(0);
629629+ expect(prev.getUTCSeconds()).toBe(0);
630630+ });
631631+632632+ it("should return current minute when called at minute boundary (9:01:00.001)", () => {
633633+ // At 9:00:00.001, after zeroing seconds it's 9:00:00
634634+ // Then subtracting 1 minute gives 8:59:00
635635+ const from = new Date("2025-03-15T09:00:00.001Z");
636636+ const prev = previousRun("* * * * *", { from });
637637+638638+ expect(prev.getUTCHours()).toBe(8);
639639+ expect(prev.getUTCMinutes()).toBe(59);
553640 });
554641 });
555642···13811468 });
1382146913831470 describe("invalid timezone", () => {
13841384- it("should return null for invalid timezone in nextRun", () => {
14711471+ it("should throw for invalid timezone in nextRun", () => {
13851472 const from = new Date("2026-03-15T14:00:00Z");
13861386- expect(nextRun("0 9 * * *", { from, timezone: "Invalid/Timezone" })).toBeNull();
14731473+ expect(() => nextRun("0 9 * * *", { from, timezone: "Invalid/Timezone" })).toThrow(
14741474+ 'Invalid timezone: "Invalid/Timezone"',
14751475+ );
13871476 });
1388147713891389- it("should return null for invalid timezone in previousRun", () => {
14781478+ it("should throw for invalid timezone in previousRun", () => {
13901479 const from = new Date("2026-03-15T14:00:00Z");
13911391- expect(previousRun("0 9 * * *", { from, timezone: "NotATimezone" })).toBeNull();
14801480+ expect(() => previousRun("0 9 * * *", { from, timezone: "NotATimezone" })).toThrow(
14811481+ 'Invalid timezone: "NotATimezone"',
14821482+ );
13921483 });
1393148413941394- it("should return false for invalid timezone in isMatch", () => {
14851485+ it("should throw for invalid timezone in isMatch", () => {
13951486 const date = new Date("2026-03-15T09:00:00Z");
13961396- expect(isMatch("0 9 * * *", date, { timezone: "Fake/Zone" })).toBe(false);
14871487+ expect(() => isMatch("0 9 * * *", date, { timezone: "Fake/Zone" })).toThrow(
14881488+ 'Invalid timezone: "Fake/Zone"',
14891489+ );
13971490 });
1398149113991399- it("should return null for empty timezone string in nextRun", () => {
14921492+ it("should throw for empty timezone string in nextRun", () => {
14001493 const from = new Date("2026-03-15T14:00:00Z");
14011401- expect(nextRun("0 9 * * *", { from, timezone: "" })).toBeNull();
14941494+ expect(() => nextRun("0 9 * * *", { from, timezone: "" })).toThrow('Invalid timezone: ""');
14021495 });
14031496 });
14041497 });