{"record":{"id":"0c7aeda7425fbf83","repo":"ionic-team/ionic-framework","slug":"invalid-hour-cycle-hourcycle-0c7aed","errorCode":null,"errorMessage":"Invalid hour cycle \"${hourCycle}\"","messagePattern":"Invalid hour cycle \"(.+?)\"","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"core/src/components/datetime/utils/format.ts","lineNumber":123,"sourceCode":"export const getFormattedHour = (hour: number, hourCycle: DatetimeHourCycle): string => {\n  /**\n   * Midnight for h11 starts at 0:00am\n   * Midnight for h12 starts at 12:00am\n   * Midnight for h23 starts at 00:00\n   * Midnight for h24 starts at 24:00\n   */\n  if (hour === 0) {\n    switch (hourCycle) {\n      case 'h11':\n        return '0';\n      case 'h12':\n        return '12';\n      case 'h23':\n        return '00';\n      case 'h24':\n        return '24';\n      default:\n        throw new Error(`Invalid hour cycle \"${hourCycle}\"`);\n    }\n  }\n\n  const use24Hour = is24Hour(hourCycle);\n  /**\n   * h23 and h24 use 24 hour times.\n   */\n  if (use24Hour) {\n    return addTimePadding(hour);\n  }\n\n  return hour.toString();\n};\n\n/**\n * Generates an aria-label to be read by screen readers\n * given a local, a date, and whether or not that date is\n * today's date.","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/ionic-team/ionic-framework/blob/625f9c38ad8c5db8a6c12df5c1622a36da90f9e3/core/src/components/datetime/utils/format.ts#L105-L141","documentation":"Thrown by getFormattedHour() in format.ts when formatting hour 0 (midnight) and hourCycle is not one of the four known cycles. getFormattedHour is used to render the labels of the ion-datetime time columns, so the throw fires during rendering exactly at the midnight boundary.","triggerScenarios":"getFormattedHour(0, hourCycle) is called with an hourCycle outside 'h11'..'h24'. This happens when the hour column is built for midnight and the cycle value was cast unsafely, left undefined by a partial state, or corrupted upstream in generateTime.","commonSituations":"Same class of issue as error 0: unsafe casts, SSR with partial locale data, or a custom wrapper that forwards a user-supplied string into the datetime without validating it.","solutions":["Constrain hourCycle to the literal union before it reaches ion-datetime; prefer letting the component resolve it from locale.","Validate the value with an allow-list guard at the boundary where you receive it.","If you call getFormattedHour yourself, default the parameter: getFormattedHour(hour, hourCycle ?? 'h12').","Audit 'as any'/'as DatetimeHourCycle' casts on hour-cycle related fields."],"exampleFix":"// before\nconst label = getFormattedHour(0, cycleFromUrl as DatetimeHourCycle);\n\n// after\nconst VALID = ['h11','h12','h23','h24'] as const;\nconst cycle = (VALID as readonly string[]).includes(cycleFromUrl) ? (cycleFromUrl as DatetimeHourCycle) : 'h12';\nconst label = getFormattedHour(0, cycle);","handlingStrategy":"type-guard","validationCode":"const OK = ['h11','h12','h23','h24'];\nconst safeCycle = OK.includes(cycle) ? cycle : 'h12';\ngetFormattedHour(0, safeCycle);","typeGuard":"function isDatetimeHourCycle(v: unknown): v is DatetimeHourCycle {\n  return typeof v === 'string' && ['h11','h12','h23','h24'].includes(v);\n}","tryCatchPattern":"try {\n  label = getFormattedHour(hour, cycle);\n} catch (e) {\n  if (e instanceof Error && /Invalid hour cycle/.test(e.message)) {\n    label = getFormattedHour(hour, 'h12');\n  } else { throw e; }\n}","preventionTips":["Default optional hourCycle parameters in your own helpers.","Validate cycle at the trust boundary (config, URL, IPC).","Keep a single source of truth for the allowed cycle list."],"tags":["datetime","hour-cycle","formatting","runtime","validation"],"backgroundTag":null,"analyzedSha":"625f9c38ad8c5db8a6c12df5c1622a36da90f9e3","analyzedAt":"2026-08-12T16:00:25.413Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}