skip to content

Date Pitfalls and UTC vs Local

Date is a mutable wrapper around epoch milliseconds with 0-indexed months, ambiguous string parsing, and getters that silently mean local time. Interviewers ask this to check that you store instants in UTC and only convert at the display edge.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

6

What does the JavaScript expression `new Date(2025, 0, 31)` represent, and which of the Date constructor's argument conventions most often cause off-by-one bugs?

level: juniorimportance: must knowfreq 72%

answer

  1. months count from zero
  2. day-of-month counts from one
  3. arguments mean local time
  4. out-of-range values roll over silently
  5. Date.UTC for the UTC reading

basics

~20 s

new Date(2025, 0, 31) is 31 January 2025 at local midnight. The month argument is zero-indexed (0 = January) while day-of-month starts at 1, and multi-argument construction is always read in the host's local time zone.

solid answer

~40 s

The multi-argument form is `new Date(year, monthIndex, day, hours, minutes, seconds, ms)`, and the fields are inconsistent on purpose-of-history: `monthIndex` counts from 0, so 0 is January and 11 is December, while `day` counts from 1. So `new Date(2025, 0, 31)` is 31 January 2025, 00:00 in whatever time zone the machine is set to — this form never means UTC. Out-of-range values do not throw; they roll over, so `new Date(2025, 12, 1)` is 1 January 2026 and `new Date(2025, 0, 0)` is 31 December 2024. Two more traps: a year between 0 and 99 is mapped into the 1900s, and omitted trailing arguments default to 1 for day and 0 for the time fields. When you want the UTC reading, build the timestamp with `Date.UTC(...)` and pass it to `new Date()`.

go deeper

for a junior

Be able to state without hesitating that the month argument starts at 0 while the day starts at 1, and that these fields are read in local time. Know that getMonth() needs +1 before display.

for a middle

Explain that the constructor normalises out-of-range fields rather than validating them, show the last-day-of-month idiom that exploits it, and reach for Date.UTC() when the value must be an absolute instant.

for a senior

Show how a locally-constructed Date leaks the server's time zone into stored data, and describe the validation wrapper you would put around user-entered calendar fields so silent rollover never reaches persistence.

for a principal

Own the rule for the codebase: where calendar fields may be assembled at all, which layer is allowed to touch local-time constructors, and why absolute instants should be built from UTC fields or parsed offsets rather than from machine-local parts.

## What a Date actually is A JavaScript `Date` object stores exactly one thing: a *time value*, a number of milliseconds since the Unix epoch, 1970-01-01T00:00:00Z. Everything else — years, months, hours — is computed on demand from that single number. The constructor's job is to turn whatever you pass into that number. ## The three constructor shapes ```js new Date(); // now new Date(1741996800000); // a time value in milliseconds new Date('2025-03-15'); // parse a string new Date(2025, 0, 31); // calendar fields, LOCAL time ``` The multi-argument form is the one this question is about. Its full signature is `new Date(year, monthIndex, day, hours, minutes, seconds, milliseconds)`; only `year` and `monthIndex` are required, `day` defaults to 1 and the time fields default to 0. ## Zero-indexed months, one-indexed days The month argument is a *month index*: 0 is January, 11 is December. Day-of-month is a real calendar day number starting at 1. There is no principle behind the mismatch — it was copied from an early Java API in 1995 and frozen by the web's backwards-compatibility rules. The practical consequence is that `new Date(2025, 12, 25)` is not Christmas; it is 25 January 2026. The same zero-indexing shows up on the read side: `getMonth()` returns 0–11, so display code needs `getMonth() + 1`, while `getDate()` (day of month) and `getFullYear()` need no adjustment. Note also that `getDate()` is the day of the month and `getDay()` is the day of the week (0 = Sunday) — a second naming trap in the same API. ## It is local time, always The calendar fields you pass are interpreted in the host environment's current time zone. On a machine set to UTC-05:00, `new Date(2025, 0, 31)` holds the time value for 2025-01-31T05:00:00Z. Run the same code on a UTC server and you get a different instant. This is why building dates from parts and then serialising them is a classic source of "the date is one day off in production". To construct from UTC fields instead, use the static `Date.UTC()`, which takes the same argument list but returns a raw time value: ```js const utcMidnight = new Date(Date.UTC(2025, 0, 31)); utcMidnight.toISOString(); // '2025-01-31T00:00:00.000Z' everywhere ``` ## Out-of-range values roll over, they do not throw The specification normalises the fields rather than validating them. `new Date(2025, 0, 32)` is 1 February 2025; `new Date(2025, 0, 0)` is 31 December 2024, because day 0 is "one before the first"; `new Date(2025, -1, 1)` is 1 December 2024. Hours, minutes and seconds roll the same way, so `new Date(2025, 0, 1, 25)` is 2 January at 01:00. This is occasionally useful — `new Date(y, m + 1, 0)` is the well-known idiom for the last day of month `m` — but it also means the constructor will never tell you that user input was nonsense. `new Date(2025, 1, 30)` silently becomes 2 March. If you need validation, check the fields you get back: ```js function makeLocalDate(y, m, d) { const dt = new Date(y, m - 1, d); const ok = dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d; return ok ? dt : null; } ``` ## The two-digit-year rule If `year` is an integer from 0 to 99 it is treated as 1900 + year, so `new Date(99, 0, 1)` is 1 January 1999 and `new Date(5, 0, 1)` is 1905. To construct a real year in the first century you must call `setFullYear()` afterwards. This rule applies only to the multi-argument form, not to `Date.parse` or ISO strings. ## Invalid Date If the resulting time value is `NaN` — or exceeds the representable range of ±8,640,000,000,000,000 ms (about ±273,790 years) — you get an *Invalid Date*: a real Date object whose `getTime()` is `NaN`. It does not throw at construction. `isNaN(d.getTime())` or `Number.isNaN(d.valueOf())` is the standard check; note that `toISOString()` on such an object throws a `RangeError`, which is a common way this surfaces late. ## What to say in an interview Name the mismatch (month from 0, day from 1), say the fields are local, mention that out-of-range values roll over instead of throwing, and reach for `Date.UTC()` the moment the value is meant to be an absolute instant rather than a wall-clock reading on this machine.

  • How would you validate that a user-entered year, month and day is a real calendar date, given that the Date constructor never rejects anything?
    Construct it, then read the fields back and compare: if `getFullYear()`, `getMonth()` and `getDate()` do not match what you passed, the constructor rolled the value over and the input was invalid. Checking `isNaN(d.getTime())` alone is not enough, because 30 February produces a perfectly valid Date object for 2 March.
  • What is the idiomatic way to get the number of days in a given month using the Date constructor?
    Rely on rollover: `new Date(year, month + 1, 0).getDate()` gives the last day of `month`, because day 0 of the next month normalises to the final day of the current one. It handles leap years automatically, since the normalisation uses the real calendar.
  • Why does `getDate()` not return the day of the week, and what does?
    `getDate()` returns the day of the month, 1–31. The day of the week is `getDay()`, returning 0 for Sunday through 6 for Saturday. The near-identical names are a well-known source of bugs; both have `getUTCDate()` and `getUTCDay()` counterparts that read the same instant in UTC.

saying these in an interview costs you the question

  • Thinks new Date(2025, 0, 31) is in December
  • Says months and days are both zero-indexed
  • Assumes the multi-argument constructor means UTC
  • Expects new Date(2025, 1, 30) to throw or be invalid
  • Thinks new Date(99, 0, 1) means the year 99

context

open as a page

A JavaScript Date stores a single number. Given that, what do `getHours()` and `getTimezoneOffset()` actually return, and why can `toISOString()` show a different calendar day than `getDate()`?

level: middleimportance: must knowfreq 63%

basics

~20 s

A Date holds only milliseconds since the epoch and carries no time zone. The plain getters project that instant into the host's local zone, the getUTC* getters project it into UTC, and getTimezoneOffset() returns UTC minus local in minutes — so it is negative east of Greenwich.

open as a page

In JavaScript, why can `new Date('2025-03-15')` and `new Date('2025-03-15T00:00:00')` refer to two different instants, and what does that mean for parsing user input?

level: middleimportance: must knowfreq 66%

basics

~20 s

A date-only ISO string is parsed as UTC midnight; the same date with a time part but no offset is parsed as local midnight. West of Greenwich the first therefore prints as the previous calendar day, which is the classic off-by-one-day date bug.

open as a page

JavaScript Date instances are mutable. What do setter methods such as `setMonth()` return and what do they do to the object, and what is the result of `new Date(2025, 0, 31).setMonth(1)`?

level: middleimportance: should knowfreq 44%

basics

~20 s

Date setters mutate the object in place and return the new time value as a number, not the Date. Setting month 1 on 31 January 2025 asks for 31 February, which normalises to 3 March — so month arithmetic on a month-end date silently skips a month.

open as a page

A JavaScript scheduler computes "the same time tomorrow" by adding `24 * 60 * 60 * 1000` to a Date. When does that produce the wrong wall-clock time, and what would you do instead?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Adding 86,400,000 milliseconds adds exactly 24 hours of elapsed time, but a local day is 23 or 25 hours long across a daylight-saving transition. On those days the wall-clock time drifts by an hour, so calendar arithmetic must use setDate(), not millisecond addition.

open as a page

You are deciding how an application should persist timestamps when its users span many time zones. Given that a JavaScript Date models only a UTC instant, when is that the right representation and when is it provably the wrong one?

level: principalimportance: should knowfreq 31%

basics

~20 s

An instant is right for things that happened: creation times, log entries, expiries, anything ordered on a timeline. It is wrong for future wall-clock commitments and for calendar dates with no time, because converting those to an instant bakes in a time-zone rule that can change or a zone that was never meant to apply.

open as a page