JavaScript’s built-in Date object is infamous for footguns: month indexes starting at zero, local versus UTC interpretation, and parsing strings that browsers treat differently. Dates and times still show up in every web app—from analytics dashboards to booking forms—so a small set of reliable patterns saves hours of production debugging.
Below is a pragmatic guide: what Date gets right, where it fails, and how to format, compare, and store instants safely. When your runtime supports it, the emerging Temporal API addresses many long-standing flaws; we cover both paths.
Mental model: instant vs local display
An instant is a single point on the timeline (store as UTC milliseconds or ISO 8601 with offset). A local display is how that instant looks in America/Chicago versus Asia/Tokyo.
Date internally stores UTC milliseconds. Methods like getHours() use the runtime’s local time zone; getUTCHours() use UTC. Mixing them in one function is a common bug source.
Rule of thumb: store and transmit UTC (or offset-aware ISO strings); convert to local only at the UI boundary.
Creating dates safely
// Preferred: explicit components (local time)
const d = new Date(2026, 10, 13, 15, 30, 0); // Nov 13, 2026 15:30 local
// UTC components
const utc = Date.UTC(2026, 10, 13, 20, 30, 0);
const fromUtc = new Date(utc);
// ISO 8601 with offset — good for APIs
const parsed = new Date("2026-11-13T15:30:00-05:00");
Avoid new Date("2026-11-13") without a time zone in older parsers—it may be treated as UTC midnight or local midnight depending on environment. For date-only values (birthdays, all-day events), store a calendar date string (YYYY-MM-DD) separately from instants, or use Temporal’s PlainDate.
Parsing user input
Never rely on Date.parse for arbitrary locale strings. Prefer:
- Structured inputs (date pickers returning ISO strings)
- Libraries with explicit locale rules (Luxon, date-fns with
parse+ known format tokens) - Intl for display, not parsing free text
If you must parse "11/03/2026", define whether it is MM/DD or DD/MM and document it in your API contract.
Formatting for humans
Native toLocaleString leverages Intl:
const instant = new Date("2026-11-13T20:00:00Z");
const label = instant.toLocaleString("en-US", {
timeZone: "America/New_York",
dateStyle: "medium",
timeStyle: "short",
});
For relative times (“3 hours ago”), compute the delta between instants, not wall-clock subtraction across DST boundaries:
function hoursAgo(from, now = new Date()) {
return (now.getTime() - from.getTime()) / (1000 * 60 * 60);
}
Comparisons and sorting
Compare with getTime() or numeric coercions:
events.sort((a, b) => a.start.getTime() - b.start.getTime());
Equality requires comparing the same unit:
const sameInstant = a.getTime() === b.getTime();
For calendar-day equality in a zone, convert both to that zone’s Y-M-D parts—do not compare UTC dates when the product meaning is “same local day.”
Arithmetic pitfalls
Adding days by mutating local fields breaks across DST:
// Risky: 24h * N ms ignores DST transitions
const risky = new Date(start);
risky.setTime(risky.getTime() + 24 * 60 * 60 * 1000);
Better: use a library that understands zones, or Temporal:
// Temporal (where available)
// const later = startPlainDate.add({ days: 7 });
For simple UTC math, millisecond addition is fine; for “add 1 calendar day in Tokyo,” zone-aware APIs are required.
ISO strings and JSON APIs
toISOString() always returns UTC with Z:
payload.createdAt = new Date().toISOString();
On read, new Date(iso) reconstructs the instant correctly. Include offsets when the source is not UTC:
{ "startsAt": "2026-11-13T09:00:00-08:00" }
Never assume the server’s time zone matches the user’s.
Time zones in the browser vs server
Browsers know the user’s IANA zone via Intl.DateTimeFormat().resolvedOptions().timeZone. Servers should receive either explicit offsets from the client or a named zone plus instant, not implicit server local time.
For recurring meetings (“every Monday 9am Eastern”), store the rule (RRULE + zone id), not a series of precomputed UTC timestamps without the rule—DST will invalidate them.
Date.now() and monotonic timers
Date.now() is wall-clock—subject to NTP adjustments. For measuring durations in the client, prefer performance.now() (monotonic in the navigation context). For server SLAs, use monotonic clocks where the OS exposes them.
Temporal: the long-term fix
The Temporal proposal (shipping in modern runtimes) splits types:
Instant— point on timelineZonedDateTime— instant viewed in a zonePlainDate/PlainTime— no time zone, for calendarsDuration— arithmetic without ambiguous Date mutation
Feature-detect and polyfill if you support older browsers:
if (typeof Temporal !== "undefined") {
const zdt = Temporal.ZonedDateTime.from("2026-11-13T10:00[America/Chicago]");
}
Temporal removes many Date mutation methods that silently change objects in place—APIs return new values, which is easier to reason about in React state.
Testing time-dependent code
- Inject a clock interface (
now()) in units under test - Freeze time with Sinon fake timers or Vitest
vi.useFakeTimers - Add explicit tests for DST transition weekends in target zones
Flaky CI often traces back to tests that call new Date() without mocking.
Common bugs checklist
- Using
getDay()(weekday 0–6) when you meantgetDate()(day of month) - Storing
Dateobjects in Redux/JSON (they serialize to strings unexpectedly) - Comparing
date1 > date2without normalizing to instants - Formatting with the server zone on SSR, then hydrating with client zone mismatch
- Assuming
getTimezoneOffset()sign—it is minutes to add to local time to get UTC
FAQ
Should I use timestamps (numbers) in the database?
Bigint epoch ms works; Postgres timestamptz and MySQL DATETIME with UTC discipline are clearer for SQL tooling.
How do I handle leap seconds?
Most systems smear or ignore leap seconds at the OS layer; do not hand-roll leap-second tables unless you operate timing-critical infra.
dayjs vs date-fns vs Luxon?
All are fine; pick one per repo. Luxon has strong zone story; date-fns is modular; dayjs is small. Consistency matters more than the choice.
Does Intl replace libraries?
For display and pluralization, often yes; for parsing and complex recurrence, still use a dedicated library or Temporal.
Ship calmer calendars
Treat instants, zones, and calendar dates as three different concepts. Store UTC at the boundary, convert for display, test DST edges once, and migrate toward Temporal where you can—the javascript date problems that haunt legacy code are mostly preventable with these habits.
Further Reading
Discover more articles on similar topics across our network

Comments
Loading comments…