Skip to content
JavaScript js dates 4 min read

Timestamps, Timezones & Temporal

A Date in JavaScript is really just a number — the count of milliseconds since a fixed point in 1970 — wrapped in a quirky, decades-old API. Most of the genuine difficulty in working with dates comes from two places: converting between that raw timestamp and human-readable values, and reasoning about time zones. This page covers Unix timestamps, the time-zone traps that catch nearly everyone, the libraries the ecosystem reaches for, and the new built-in Temporal API that is poised to replace Date entirely.

Unix timestamps

A Unix timestamp is the number of seconds (or in JavaScript, milliseconds) elapsed since the Unix epoch: midnight UTC on 1 January 1970. Because it is a single absolute number with no time zone attached, it is the safest way to store and transmit an instant in time. JavaScript exposes it through Date.now() and getTime().

const now = Date.now(); // milliseconds since epoch
console.log(now);
console.log(Math.floor(now / 1000)); // seconds — what most backends use

const d = new Date("2026-03-14T15:30:00Z");
console.log(d.getTime()); // ms for that specific instant
console.log(new Date(1773502200000).toISOString()); // back from ms

Output:

1781434200000
1781434200
1773502200000
2026-03-14T15:30:00.000Z

The critical distinction: JavaScript works in milliseconds, while Unix tools, databases, and many APIs use seconds. Mixing them silently produces dates in 1970 or 56000. Always convert deliberately, and prefer storing UTC timestamps over formatted strings.

Tip: A timestamp is timezone-agnostic by definition — it is the same number everywhere on Earth. Store instants as UTC timestamps (or ISO 8601 strings with a Z), and apply a time zone only when displaying to a user.

Timezone pain points

A Date object stores a UTC instant internally, but most of its getter methods (getHours(), getDate(), etc.) silently apply the runtime’s local time zone. This means the same code prints different results on a server in New York versus a laptop in Tokyo, which is a constant source of bugs.

const d = new Date("2026-03-14T23:30:00Z");

// Depends on the machine's local zone — NOT deterministic
console.log(d.getHours());

// Deterministic: explicitly pick the zone you mean
console.log(
  d.toLocaleString("en-US", {
    timeZone: "America/New_York",
    hour: "numeric",
    minute: "2-digit",
  })
);

Output:

23
7:30 PM

Native Date has no way to construct a time in an arbitrary zone, no concept of a “zoned” value you can do math on, and no built-in handling of daylight saving transitions. You can read a zoned representation via Intl, but you cannot do arithmetic in that zone. These gaps are exactly what libraries and Temporal exist to fill.

Warning: Never parse a date-only string like "2026-03-14" and assume local midnight. JavaScript parses bare date strings as UTC midnight, so in negative-offset zones new Date("2026-03-14").getDate() can return 13.

Libraries: date-fns, Luxon, Day.js

Until Temporal ships everywhere, libraries remain the pragmatic choice. The big three differ in philosophy and bundle size.

LibraryStyleBundle (min+gz)Time zonesNotes
date-fnsPure functions, tree-shakable~12 kB (typical subset)Via date-fns-tz add-onWorks on native Date; import only what you use
LuxonImmutable DateTime class~25 kBFirst-class (IANA via Intl)Best zone/DST support of the three
Day.jsTiny, Moment-like API~3 kB coreVia pluginSmallest; plugin-based features

Luxon shines when time zones and DST matter:

import { DateTime } from "luxon";

const meeting = DateTime.fromISO("2026-03-14T15:30:00", {
  zone: "America/New_York",
});

console.log(meeting.toUTC().toISO());
console.log(meeting.setZone("Asia/Tokyo").toFormat("yyyy-MM-dd HH:mm ZZZZ"));

Output:

2026-03-14T19:30:00.000Z
2026-03-15 04:30 GMT+9

date-fns is ideal when you already have Date objects and want minimal weight, since you import only the functions you call. Note that Moment.js, the original library, is now in maintenance mode — do not start new projects with it.

The Temporal API

Temporal is a new built-in global (Stage 3, shipping across engines) designed to replace Date outright. It is immutable, has separate purpose-built types, and treats time zones and calendars as first-class. Its core types are Temporal.Instant (an exact moment), Temporal.ZonedDateTime (an instant in a specific zone), Temporal.PlainDate/PlainTime/PlainDateTime (wall-clock values with no zone), and Temporal.Duration.

// Available natively where Temporal has shipped, or via @js-temporal/polyfill
const zdt = Temporal.ZonedDateTime.from({
  timeZone: "America/New_York",
  year: 2026,
  month: 3,
  day: 14,
  hour: 15,
  minute: 30,
});

const later = zdt.add({ hours: 26 }); // DST-aware arithmetic
console.log(zdt.toInstant().toString());
console.log(later.toString());
console.log(zdt.until(later).toString()); // a Temporal.Duration

Output:

2026-03-14T19:30:00Z
2026-03-15T18:30:00-04:00[America/New_York]
PT26H

Because Temporal objects are immutable, every operation returns a new value — no accidental mutation. Adding 26 hours across a daylight-saving boundary correctly accounts for the lost or gained hour, something native Date cannot do. Until support is universal, use the official @js-temporal/polyfill.

Best Practices

  • Store and transmit instants as UTC timestamps or ISO 8601 strings with a Z; never store formatted, localized strings.
  • Treat seconds-vs-milliseconds explicitly — JavaScript uses ms, most backends use seconds.
  • Avoid Date’s local getters (getHours, getDate) for anything that must be deterministic; pass an explicit timeZone to Intl instead.
  • Reach for Luxon when time zones and DST are central; choose date-fns or Day.js when bundle size dominates.
  • Do not start new projects on Moment.js — it is in maintenance mode.
  • Prefer Temporal (via the polyfill today) for new code that needs zoned arithmetic, immutability, or calendar support.
  • Never assume a bare "YYYY-MM-DD" string means local midnight — JavaScript parses it as UTC.
Last updated June 1, 2026
Was this helpful?