Date and Time Pitfalls
JS Date gotchas: parsing strings, time zones, month indexes, and when to reach for Temporal or a library.
- 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
- Store UTC instants (ISO Z or epoch ms) in APIs and DBs.
- Convert to local only at the display edge with
Intlor explicit timeZone. - Never parse
MM/DD/YYYYwithnew Date(string). - Remember month = 0.
- 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.”
Related
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
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.