JavaScript/TypeScript timezone handling best practices, focusing on JST(UTC+9) <-> UTC conversion.
Use this skill whenever the user is working with timezone conversion in JS/TS, dealing with
Date object timezone issues, implementing DateRangePicker or date input components that need
TZ-aware handling, debugging "9 hours off" bugs, using toLocaleString or Intl.DateTimeFormat
with timezone concerns, or storing/retrieving dates between browser and DB.
Also trigger when you see anti-patterns like `new Date(year, month, day)` used for
cross-timezone scenarios, `setHours`/`setMinutes` for timezone conversion, or
`toLocaleString` without a `timeZone` option.
Even if the user doesn't mention "timezone" explicitly, if they're dealing with date/time
mismatches between client and server, or dates shifting by hours when saved to DB, this skill applies.
JavaScript/TypeScript timezone handling best practices, focusing on JST(UTC+9) <-> UTC conversion.
Use this skill whenever the user is working with timezone conversion in JS/TS, dealing with
Date object timezone issues, implementing DateRangePicker or date input components that need
TZ-aware handling, debugging "9 hours off" bugs, using toLocaleString or Intl.DateTimeFormat
with timezone concerns, or storing/retrieving dates between browser and DB.
Also trigger when you see anti-patterns like `new Date(year, month, day)` used for
cross-timezone scenarios, `setHours`/`setMinutes` for timezone conversion, or
`toLocaleString` without a `timeZone` option.
Even if the user doesn't mention "timezone" explicitly, if they're dealing with date/time
mismatches between client and server, or dates shifting by hours when saved to DB, this skill applies.
JS/TS Timezone Best Practices (JST <-> UTC)
Core Problem
Date internally holds a UTC timestamp (ms since 1970-01-01T00:00:00Z), but many constructors and methods implicitly use the browser's local timezone. The same code produces different UTC values depending on the user's TZ setting:
This is the root cause of "9 hours off" bugs in JST-targeted services.
Scope note: JST is a fixed +9 offset with no daylight saving time, so the hour - 9 / + 9h arithmetic below is safe. For timezones with DST, use Intl.DateTimeFormat instead of fixed offset math.
Three Rules
Create dates with Date.UTC — never new Date(y, m, d, h, min) for cross-TZ scenarios
Read date parts with getUTC* methods — never getHours(), getDate(), etc.
Display with explicit timeZone option — never bare toLocaleString("ja-JP")
Correct Patterns
1. User Input (JST) -> UTC Date
When a user picks "2026-01-20 21:00" in a DateRangePicker, that's JST. Convert to UTC:
The string "2026-01-20T21:00:00" (no Z, no offset) is parsed as local time — already environment-dependent. Subtracting 9h on top of that only works if the browser happens to be in JST.
Fix: use Date.UTC from the start, or always include timezone in ISO strings (Z or +09:00).
Missing timeZone in display
// BAD: shows different times for users in different TZs
date.toLocaleString("ja-JP"); // no timeZone option
Fix: always pass timeZone: "Asia/Tokyo" (or the appropriate zone).
TZ-ambiguous ISO strings
// DANGEROUS: no TZ indicator, parsed as local timenewDate("2026-01-20T21:00:00");
// SAFE: explicit TZnewDate("2026-01-20T21:00:00Z"); // UTCnewDate("2026-01-20T21:00:00+09:00"); // JST
Boundary Value Tests
JST 00:00-08:59 maps to the previous UTC day. This is where most bugs hide:
JST Input
Expected UTC
Why it matters
2026/01/21 00:00
2026/01/20 15:00
Date rolls back
2026/01/21 08:59
2026/01/20 23:59
Just before same-day cutoff
2026/01/21 09:00
2026/01/21 00:00
Same-day boundary
2026/01/01 00:00
2025/12/31 15:00
Year rolls back
2024/02/29 00:00
2024/02/28 15:00
Leap year + day rollback
When implementing timezone conversion, write tests covering these boundaries. The Date.UTC underflow handling makes them pass naturally, but explicit tests prevent regressions.
Data Flow Summary
User Input (JST) --[Date.UTC, hour-9]--> Date (UTC internally) --[save]--> DB (UTC)
DB (UTC) --[fetch]--> Date (UTC internally) --[timeZone:"Asia/Tokyo"]--> Display (JST)
Keep this flow unidirectional. Never mix local-TZ methods into the pipeline.
Future: Temporal API
The Temporal API (TC39 stage 3) will eventually replace Date with explicit timezone-aware types like Temporal.ZonedDateTime. For new projects where Temporal is available, prefer it over Date. Until then, the patterns above are the safest approach.