JavaScript Date Objects

6 minute read

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 (…Z for UTC)
  • ✅ Use UTC setters/getters for arithmetic (setUTCDate, getUTCFullYear, …)
  • ✅ Use Intl.DateTimeFormat for display, cached when possible
  • ✅ Treat Date objects as immutable — clone before mutating
  • ✅ Compare via +date1 === +date2 (or getTime())
  • ❌ Don’t trust Date.parse on 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.

← Previous: JS Object Methods Next: JS Math Object →

Updated: