JavaScript Date Objects
JavaScript’s built-in Date is the language’s most-criticised API — but until Temporal ships universally, it’s what we have, and you can still get reliable behaviour out of it if you know its rules. This chapter covers creating dates, parsing, formatting for humans, doing arithmetic, and the timezone gotchas that catch every team eventually.
Anatomy of a Date
Internally a Date is a single number: milliseconds since the Unix epoch (1970-01-01T00:00:00 UTC).
const now = new Date();
now.getTime(); // 1750000000000 — ms since epoch
+now; // 1750000000000 — same, via valueOf()
Number(now); // 1750000000000
// Wall-clock conversion happens at access time using the local timezone
UTC instant (a number)
│
▼
┌────────────────────┐
│ Date object │
│ value = 1.75e12 ms │
└────────────────────┘
│ getHours() etc.
▼
Local timezone view
Creating a Date
new Date() // now
new Date(0) // 1970-01-01T00:00:00.000Z
new Date(1_750_000_000_000) // from ms-since-epoch
new Date("2026-05-16") // ISO string
new Date("2026-05-16T10:00:00Z") // ISO with timezone (UTC)
new Date(2026, 4, 16) // year, MONTH(0-based!), day → May 16
new Date(2026, 4, 16, 10, 0, 0) // y, m, d, h, mi, s
// Date.now() — number, not Date
Date.now(); // 1750000000000
// Date.UTC — builds ms-since-epoch from UTC components
new Date(Date.UTC(2026, 4, 16)); // exact UTC instant
Warning: Months are 0-indexed. January = 0, December = 11. The day-of-month, year, hours, minutes, seconds, and ms are all 1- or 0-indexed normally — only months are zero-based. Mistakes here cause off-by-one-month bugs constantly.
ISO Strings — The Only Reliable Text Format
"2026-05-16" // date-only — interpreted as UTC midnight
"2026-05-16T10:00:00" // local time (engine-dependent historically)
"2026-05-16T10:00:00Z" // ✅ UTC, unambiguous
"2026-05-16T10:00:00+05:30" // ✅ explicit offset
"2026-05-16T10:00:00.123Z" // with milliseconds
new Date("2026-05-16").toISOString();
// "2026-05-16T00:00:00.000Z"
new Date().toISOString();
// e.g., "2026-06-16T14:32:01.456Z"
Tip: Always store and transmit dates as ISO 8601 strings with timezone (e.g., …Z for UTC). Anything else is a bug waiting to happen.
Reading Components
| Method | Local | UTC |
|---|---|---|
| Year | getFullYear() |
getUTCFullYear() |
| Month (0-11) | getMonth() |
getUTCMonth() |
| Day of month | getDate() |
getUTCDate() |
| Day of week (0=Sun) | getDay() |
getUTCDay() |
| Hours | getHours() |
getUTCHours() |
| Minutes | getMinutes() |
getUTCMinutes() |
| Seconds | getSeconds() |
getUTCSeconds() |
| Milliseconds | getMilliseconds() |
getUTCMilliseconds() |
| ms since epoch | getTime() / valueOf() |
(same — always UTC) |
| Timezone offset (min) | getTimezoneOffset() |
(same) |
const d = new Date("2026-05-16T10:00:00Z");
d.getFullYear(); // 2026
d.getMonth(); // 4 (May!)
d.getDate(); // depends on local TZ
d.getUTCDate(); // 16
d.getDay(); // 0=Sun … 6=Sat
d.getTimezoneOffset(); // minutes east of UTC, INVERTED sign — IST (+5:30) returns -330
Warning: getTimezoneOffset() returns minutes with the opposite sign to what you might expect. UTC+5:30 returns -330, not 330. Blame the spec.
Setting Components
Mutating setters return the new ms-since-epoch:
const d = new Date("2026-05-16T10:00:00Z");
d.setUTCDate(20); // → 2026-05-20T10:00:00Z
d.setUTCMonth(11); // → 2026-12-20T10:00:00Z
d.setUTCFullYear(2030); // → 2030-12-20T10:00:00Z
d.setUTCHours(0, 0, 0, 0); // → 2030-12-20T00:00:00Z (start of day)
Setters auto-normalize:
const d = new Date(2026, 0, 32); // Jan 32 → Feb 1
d.toDateString(); // "Sun Feb 01 2026"
This is occasionally useful — setDate(0) jumps to the last day of the previous month:
const lastDayOfMay = new Date(2026, 5, 0); // June 0 → May 31
Date Arithmetic
Subtract two dates — you get milliseconds:
const start = new Date("2026-05-16T10:00:00Z");
const end = new Date("2026-05-16T11:30:00Z");
end - start; // 5400000 ms
(end - start) / 1000; // 5400 s
(end - start) / 1000 / 60; // 90 min
// Add one day
const tomorrow = new Date(start.getTime() + 86_400_000);
// Add via setters (auto-normalises across month boundaries)
const next = new Date(start);
next.setUTCDate(next.getUTCDate() + 1);
Comparisons work because of valueOf():
new Date("2026-05-16") < new Date("2026-06-01"); // true
new Date("2026-05-16") === new Date("2026-05-16"); // false ← different objects!
+new Date("2026-05-16") === +new Date("2026-05-16"); // true ← compare numbers
Formatting for Humans
Built-in formatters:
const d = new Date("2026-05-16T10:00:00Z");
d.toString(); // engine-locale full string ("Sat May 16 2026 10:00:00 GMT+…")
d.toDateString(); // "Sat May 16 2026"
d.toTimeString(); // "10:00:00 GMT+…"
d.toISOString(); // "2026-05-16T10:00:00.000Z" ← always UTC
d.toJSON(); // same as toISOString
d.toUTCString(); // "Sat, 16 May 2026 10:00:00 GMT"
d.toLocaleString(); // locale, system TZ
d.toLocaleDateString(); // date only
d.toLocaleTimeString(); // time only
Locale-aware formatting via Intl.DateTimeFormat — preferred for production:
const fmt = new Intl.DateTimeFormat("en-GB", {
dateStyle: "long",
timeStyle: "short",
timeZone: "Asia/Kolkata",
});
fmt.format(new Date("2026-05-16T10:00:00Z"));
// "16 May 2026 at 15:30"
Custom parts:
new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "short",
day: "2-digit",
hour: "2-digit",
minute:"2-digit",
hour12: true,
}).format(new Date("2026-05-16T10:00:00Z"));
// "May 16, 2026, 10:00 AM"
For repeated formatting, always cache the formatter — constructing it is the expensive part.
Relative Time
const rel = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
rel.format(-1, "day"); // "yesterday"
rel.format( 1, "day"); // "tomorrow"
rel.format(-3, "hour"); // "3 hours ago"
rel.format( 2, "month"); // "in 2 months"
A small “time-ago” helper:
function timeAgo(date, now = Date.now()) {
const diffMs = +date - now;
const sec = Math.round(diffMs / 1000);
const min = Math.round(sec / 60);
const hr = Math.round(min / 60);
const day = Math.round(hr / 24);
const fmt = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
if (Math.abs(sec) < 60) return fmt.format(sec, "second");
if (Math.abs(min) < 60) return fmt.format(min, "minute");
if (Math.abs(hr) < 24) return fmt.format(hr, "hour");
return fmt.format(day, "day");
}
timeAgo(new Date(Date.now() - 30_000)); // "30 seconds ago"
timeAgo(new Date(Date.now() + 3_600_000)); // "in 1 hour"
Common Tasks
// Start of day (UTC)
function startOfDayUTC(d = new Date()) {
const x = new Date(d);
x.setUTCHours(0, 0, 0, 0);
return x;
}
// Add days
function addDays(date, days) {
const x = new Date(date);
x.setUTCDate(x.getUTCDate() + days);
return x;
}
// Days between two dates (ignoring time)
function daysBetween(a, b) {
const ms = startOfDayUTC(b) - startOfDayUTC(a);
return Math.round(ms / 86_400_000);
}
daysBetween(new Date("2026-05-16"), new Date("2026-05-20")); // 4
// Is date today?
function isToday(d) {
const today = new Date();
return d.toDateString() === today.toDateString();
}
A Realistic Example — Event in Multiple Timezones
function showEventTimes(isoUtc) {
const date = new Date(isoUtc);
const zones = ["UTC", "America/New_York", "Europe/London", "Asia/Kolkata", "Asia/Tokyo"];
return zones.map(tz => ({
zone: tz,
when: new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
timeStyle: "short",
timeZone: tz,
}).format(date),
}));
}
showEventTimes("2026-05-16T14:00:00Z");
// [
// { zone: "UTC", when: "May 16, 2026, 2:00 PM" },
// { zone: "America/New_York", when: "May 16, 2026, 10:00 AM" },
// { zone: "Europe/London", when: "May 16, 2026, 3:00 PM" },
// { zone: "Asia/Kolkata", when: "May 16, 2026, 7:30 PM" },
// { zone: "Asia/Tokyo", when: "May 16, 2026, 11:00 PM" },
// ]
Common Mistakes
// ❌ Comparing Date objects with ===
new Date(0) === new Date(0); // false
+new Date(0) === +new Date(0); // true ← compare ms
// ❌ Forgetting months are 0-indexed
new Date(2026, 5, 1); // June 1, NOT May 1
// ❌ Parsing ambiguous local strings
new Date("2026-05-16 10:00:00"); // ✗ engine-dependent
new Date("2026-05-16T10:00:00Z"); // ✓
// ❌ Mutating shared dates
const a = new Date();
const b = a;
b.setDate(1);
// `a` was just changed too — they're the same object!
// ✅ Clone first
const b = new Date(a);
Tip: A modern alternative — the Temporal API — is reaching browsers and Node. It fixes nearly every quirk in this chapter (immutable, real timezone support, no 0-indexed months). Until adoption is universal, stick with Date + Intl.DateTimeFormat. Watch for Temporal arriving as the new default.
Best Practices Checklist
- ✅ Always store/transmit timestamps as ISO 8601 with timezone (
…Zfor UTC) - ✅ Use UTC setters/getters for arithmetic (
setUTCDate,getUTCFullYear, …) - ✅ Use
Intl.DateTimeFormatfor display, cached when possible - ✅ Treat
Dateobjects as immutable — clone before mutating - ✅ Compare via
+date1 === +date2(orgetTime()) - ❌ Don’t trust
Date.parseon non-ISO strings - ❌ Don’t forget months are 0-indexed
- ❌ Don’t mix local and UTC getters within one calculation
What’s Next?
Time covered. Next, the Math object — JavaScript’s static numeric utility belt. Rounding, trig, randomness, exponents, all in one place.