{"record":{"id":"b0e64d127fd92ead","repo":"n8n-io/n8n","slug":"failed-to-evaluate-cron-expression-json-stringif","errorCode":null,"errorMessage":"Failed to evaluate cron expression ${JSON.stringify(cronExpression)} in timezone ${JSON.stringify(timezone)}: ${(error as Error).message}","messagePattern":"Failed to evaluate cron expression (.+?) in timezone (.+?): (.+?)","errorType":"validation","errorClass":"InvalidScheduleError","httpStatus":null,"severity":"error","filePath":"packages/@n8n/scheduler/src/core/recurrence/kinds/cron.ts","lineNumber":90,"sourceCode":" * A cursor over the cron fires strictly after `after`, in the given IANA\n * timezone. `cron-parser` resolves DST via luxon (wall-clock): a nonexistent\n * local time (spring-forward) shifts forward, a repeated one (fall-back) fires\n * once.\n * @param cronExpression The cron expression to evaluate.\n * @param after The instant the cursor starts firing after (strictly).\n * @param timezone IANA zone the expression is evaluated in.\n * @returns A cursor whose `next()` yields successive fires.\n * @throws {InvalidScheduleError} When the expression cannot be parsed.\n */\nexport function parseCron(\n\tcronExpression: CronExpression,\n\tafter: Date,\n\ttimezone: string,\n): CronCursor {\n\ttry {\n\t\treturn CronExpressionParser.parse(cronExpression, { currentDate: after, tz: timezone });\n\t} catch (error) {\n\t\tthrow new InvalidScheduleError(\n\t\t\t`Failed to evaluate cron expression ${JSON.stringify(cronExpression)} in timezone ${JSON.stringify(timezone)}: ${(error as Error).message}`,\n\t\t);\n\t}\n}\n\n/**\n * The next cron fire strictly after `after`.\n * @param schedule The cron schedule. A `recurring_cron` works too: its first\n * fire ignores the \"every N\" rule (there is no previous fire to count from yet).\n * @param after The instant to fire after.\n * @returns The matching instant; cron is unbounded, so this is never `null`.\n */\nexport function cronNextRun(schedule: CronSchedule | RecurringCronSchedule, after: Date): Date {\n\treturn parseCron(schedule.cronExpression, after, resolvedTimezone(schedule)).next().toDate();\n}\n\n/**\n * Every cron fire starting at `first`, oldest first.","sourceCodeStart":72,"sourceCodeEnd":108,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/scheduler/src/core/recurrence/kinds/cron.ts#L72-L108","documentation":"Thrown by parseCron when CronExpressionParser.parse fails AT EVALUATION TIME (with currentDate and tz bound), as opposed to error 936 which fires at static validation. parseCron is the cursor factory used by nextRun helpers; wrapping here adds the timezone and expression context that the raw parser error lacks.","triggerScenarios":"Calling parseCron(cronExpression, after, timezone) where the expression is parseable in isolation but fails when bound to a specific currentDate/timezone - typically because the parser rejects the combination (e.g. a 6-field seconds expression evaluated in a zone with DST transition at exactly `after`).","commonSituations":"A DST transition coinciding with the requested `after` instant; an expression that is valid syntactically but yields no fires in the configured timezone; a timezone that became invalid between validation and evaluation (rare).","solutions":["Read the wrapped error - it carries both expression and timezone.","If the failure is DST-related, pass an `after` that is not exactly on a transition instant, or evaluate in UTC and convert.","Re-validate with validateCron first to surface the cleaner error 936 message before reaching parseCron.","Confirm the timezone string is still a valid IANA zone at evaluation time."],"exampleFix":"// before\nconst cursor = parseCron(expr, new Date('2024-03-31T02:30:00'), 'Europe/Berlin'); // DST gap -> throws\n\n// after - evaluate in UTC, then convert fires to the target zone\nconst cursor = parseCron(expr, new Date('2024-03-31T02:30:00'), 'UTC');\nconst fireUtc = cursor.next().toDate();\nconst fireLocal = DateTime.fromJSDate(fireUtc, { zone: 'Europe/Berlin' }).toJSDate();","handlingStrategy":"try-catch","validationCode":"function safeParseCron(expr: string, after: Date, timezone: string) {\n  try {\n    return parseCron(expr, after, timezone);\n  } catch (e) {\n    // Fall back to UTC evaluation, then convert - sidesteps DST-gap failures.\n    const utc = parseCron(expr, after, 'UTC');\n    return utc; // caller converts fires back to target zone\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  const cursor = parseCron(cronExpression, after, timezone);\n} catch (e) {\n  if (e instanceof InvalidScheduleError && /Failed to evaluate/.test(e.message)) {\n    // Likely DST or zone-specific; retry in UTC and convert fires.\n    const utcCursor = parseCron(cronExpression, after, 'UTC');\n    return convertFires(utcCursor, timezone);\n  }\n  throw e;\n}","preventionTips":["Call validateCron first to surface the cleaner error 936 message before evaluation.","Avoid passing an `after` instant that lands exactly on a DST transition.","When in doubt, evaluate in UTC and convert fires to the target zone.","Confirm the timezone string is still a valid IANA zone at evaluation time."],"tags":["scheduler","cron","timezone","dst"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}