Dates, Times & Temporal
Advanced · 18 min read · ▶ live playground · ✦ checkpoint
Dates look ordinary until you ship a reminder for July and it lands in August. JavaScript's old Date API can represent real instants, but its design has potholes: zero-indexed months, local time defaults, and time zones hiding in plain sight.
Date stores an instant
A Date object is JavaScript's old built-in object for representing one instant in time. Think of a Date as a shipping label stamped with one exact moment on the global timeline; different cities may read that label on different wall clocks, but the instant is the same.
The cleanest string form for an instant is an ISO string, a standard date-time string such as "2026-07-04T15:30:00.000Z". The Z means UTC, the world's common clock for timestamps.
const launch = new Date("2026-07-04T15:30:00.000Z");
console.log(launch.toISOString());
console.log(launch.getTime());
console.log(Date.UTC(2026, 6, 4, 15, 30));→ 2026-07-04T15:30:00.000Z, 1783179000000, then 1783179000000.
A timestamp is a number that counts milliseconds since midnight at the start of January 1, 1970 in UTC. getTime() returns that number. Date.UTC(...) builds the same kind of timestamp from parts.
Those two lines matching is the useful model: a Date stores an instant as a timestamp. Formatting is a separate question.
The zero-indexed month trap
The weirdest Date constructor rule is the month number. In new Date(year, monthIndex, day) and Date.UTC(year, monthIndex, day), January is 0, February is 1, and July is 6.
const mistakenBirthday = new Date(Date.UTC(2026, 7, 4));
const intendedBirthday = new Date(Date.UTC(2026, 6, 4));
console.log(mistakenBirthday.toISOString().slice(0, 10));
console.log(intendedBirthday.toISOString().slice(0, 10));→ 2026-08-04, then 2026-07-04.
This is why ISO strings are often safer when you already have a full date. Humans write July as 07; the numeric Date constructor wants 6.
Time zones change the calendar
A time zone is a region's rulebook for turning an instant into local clock time. The same instant can be Saturday morning in one place and Sunday just after midnight in another.
const meeting = new Date("2026-07-04T15:30:00.000Z");
const newYorkTime = new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
timeStyle: "short",
timeZone: "America/New_York",
});
const tokyoTime = new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
timeStyle: "short",
timeZone: "Asia/Tokyo",
});
console.log(newYorkTime.format(meeting));
console.log(tokyoTime.format(meeting));→ Jul 4, 2026, 11:30 AM, then Jul 5, 2026, 12:30 AM.
Nothing about the instant changed. Only the time-zone rulebook changed. This is the pain behind "it works on my machine" date bugs: if you format without saying the time zone, JavaScript uses the user's environment.
Format with Intl.DateTimeFormat
Date can produce strings, but user-facing formatting belongs to Intl.DateTimeFormat, the built-in formatter that understands locales, calendars, and time zones.
const matchTime = new Date("2026-07-04T15:30:00.000Z");
const formatter = new Intl.DateTimeFormat("en-US", {
weekday: "short",
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
timeZone: "UTC",
timeZoneName: "short",
});
console.log(formatter.format(matchTime));→ Sat, Jul 4, 3:30 PM UTC.
The important habit is not memorizing every option. It is separating jobs:
- Store instants as timestamps or ISO strings.
- Format for humans with
Intl.DateTimeFormat. - Pass
timeZonewhen the zone matters.
Try it — one instant, two clocks
Run this, then change "Europe/London" to "America/Los_Angeles". You are formatting the same instant for a different wall clock.
Temporal is the replacement
Date tries to handle too many jobs with one object. Temporal is the modern JavaScript date-time API that separates those jobs into clearer types: exact instants, calendar dates, wall-clock times, and time-zone-aware moments.
Temporal is part of ES2026. It is already shipping in Firefox 139, Chrome 144, Edge 144, and Node 26 by default. Safari stable still lacks it, so production apps that need it today use the temporal-polyfill package.
Here is the shape you use in a runtime with Temporal support or with the polyfill installed:
import { Temporal } from "temporal-polyfill";
const meeting = Temporal.ZonedDateTime.from({
timeZone: "America/New_York",
year: 2026,
month: 7,
day: 4,
hour: 11,
minute: 30,
});
const tokyoMeeting = meeting.withTimeZone("Asia/Tokyo");
console.log(meeting.toString());
console.log(tokyoMeeting.toString());Notice the difference: month: 7 means July in Temporal. Temporal's API uses human month numbers, and its type names force you to say what kind of time value you mean.
Do not start new projects with Moment.js
Moment.js mattered. It carried a generation of JavaScript apps through years when Date was painful and Intl was weaker. But it is not the right default for new work now.
For new projects, use platform tools first: Intl.DateTimeFormat for formatting, timestamps or ISO strings for stored instants, and Temporal or temporal-polyfill for serious date math and time-zone work. If you inherit Moment in an older app, maintain it carefully. If you are starting fresh, do not add it.
Checkpoint
Answer all three to mark this lesson complete
You now have the honest map: Date is legacy but still everywhere, Intl formats for people, and Temporal is the future for serious time work. Next, you put Part III together in an async machine that coordinates timed sources, cancellation-shaped flows, and clear state updates.