ESC

Type to search the knowledge base.

Date and Time Pitfalls

JS Date gotchas: parsing strings, time zones, month indexes, and when to reach for Temporal or a library.

intermediate3 min read
  • javascript
  • date
  • time
  • timezone

Date is one of the oldest footguns in the language. Month indexes are zero-based, string parsing is implementation-defined for non-ISO forms, and local vs UTC confusion ships bugs that only appear in another country. Know the traps; prefer explicit UTC or a modern API for new code.

Construction

new Date(); // now
new Date(0); // Unix epoch ms
new Date(2026, 0, 15); // Jan 15 2026 LOCAL — month 0 = January
new Date(Date.UTC(2026, 0, 15)); // UTC midnight components → Date

Date.now(); // ms number
Date.parse('2026-01-15T00:00:00Z'); // ms or NaN

Month is 0–11. Day is 1–31. That asymmetry alone causes annual December bugs.

Parsing: only trust ISO UTC

// reliable
new Date('2026-01-15T12:00:00.000Z');

// date-only ISO is treated as UTC midnight in ES5+ engines
new Date('2026-01-15');

// unreliable / legacy — avoid
new Date('01/15/2026');
new Date('Jan 15, 2026');

If the backend sends timestamps, prefer Unix ms or full ISO-8601 with offset/Z. Don’t invent locale string formats on the wire.

Local vs UTC getters

const d = new Date('2026-01-15T00:00:00Z');

d.getUTCFullYear(); // 2026
d.getUTCMonth();    // 0
d.getFullYear();    // depends on local TZ — may be 2025 in US evenings
d.getMonth();

UI “calendar day in the user’s zone” uses local getters. “Instant on a timeline” stays in UTC or stores an offset separately.

Math and DST

// adding 24 hours ≠ adding one calendar day across DST
const next = new Date(d.getTime() + 24 * 60 * 60 * 1000);

// safer calendar +1 day in local
const local = new Date(d);
local.setDate(local.getDate() + 1);

Duration math across zones is why libraries (Luxon, date-fns-tz) and Temporal exist.

Formatting

d.toISOString(); // always UTC
d.toLocaleDateString('en-US', { dateStyle: 'medium' });
d.toLocaleString('en-GB', { timeZone: 'UTC' });

// Intl is the right tool for display
new Intl.DateTimeFormat('de-DE', {
  dateStyle: 'full',
  timeZone: 'Europe/Berlin',
}).format(d);

Don’t hand-roll YYYY-MM-DD with local getters unless you mean local:

function toISODateUTC(d) {
  return d.toISOString().slice(0, 10);
}

Invalid dates

const bad = new Date('nope');
Number.isNaN(bad.getTime()); // true
bad.toISOString(); // RangeError

Always validate before formatting.

Practical rules

  1. Store UTC instants (ISO Z or epoch ms) in APIs and DBs.
  2. Convert to local only at the display edge with Intl or explicit timeZone.
  3. Never parse MM/DD/YYYY with new Date(string).
  4. Remember month = 0.
  5. For new greenfield date logic, track Temporal / polyfills as support matures.

Interview answer

“Date months are zero-based; only ISO-8601 strings are reliably parseable. I keep UTC on the wire, format with Intl and an explicit timeZone, and avoid adding 24h for calendar arithmetic across DST. Invalid dates have NaN time and throw on toISOString.”

Server timestamps and clocks

// API: always prefer absolute instants
// { "createdAt": "2026-08-04T12:00:00.000Z" }
// or { "createdAt": 1754308800000 }

function formatMessageTime(iso, timeZone) {
  return new Intl.DateTimeFormat(undefined, {
    timeStyle: 'short',
    dateStyle: 'medium',
    timeZone,
  }).format(new Date(iso));
}

Client clocks are wrong surprisingly often. For “time ago”, compute deltas from a server-provided Date header or include serverNow in the payload if correctness matters (auctions, OTPs). Never store “local wall time without zone” if you need to reconstruct an instant later.

Further reading

Related guides