skip to content

Dates and Internationalization

Time and locale formatting are where naive code breaks first: the legacy Date object, the Temporal replacement, and the Intl family for formatting numbers, dates and sorted text. Interviewers ask because time-zone and locale bugs are expensive and extremely common.

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

explore

questions

18

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

In JavaScript's Temporal API, what do Temporal.Instant, Temporal.PlainDateTime and Temporal.ZonedDateTime each model, and what extra information does converting between them require?

level: middleimportance: must knowfreq 50%

basics

~20 s

Temporal.Instant is a fixed point on the global timeline with no zone or calendar. Temporal.PlainDateTime is a wall-clock reading with no zone, so it names no exact time. Temporal.ZonedDateTime is both together, carrying an IANA time-zone ID.

open as a page

On a Temporal.ZonedDateTime, why can zdt.add({ days: 1 }) and zdt.add({ hours: 24 }) give different results, and how does Temporal resolve a wall-clock time that a DST transition erased?

level: seniorimportance: must knowfreq 45%

basics

~20 s

Temporal adds date units in calendar space and time units in exact-elapsed time, so across a DST shift one day may be 23 or 25 real hours. A wall-clock time that no longer exists is resolved by the disambiguation option, which defaults to compatible.

open as a page

In JavaScript, what does calling Number.prototype.toLocaleString() or Date.prototype.toLocaleDateString() with no arguments actually do, and how do those methods relate to the Intl constructors?

level: juniorimportance: should knowfreq 45%

basics

~10 s

Both delegate to Intl: toLocaleString builds an Intl.NumberFormat, toLocaleDateString an Intl.DateTimeFormat, taking the same locales and options arguments. With no arguments each uses the runtime's default locale, which is host-dependent.

open as a page

Temporal.PlainDate objects are immutable. What does date.add({ days: 1 }) return, and how do you produce a copy with a different month?

level: juniorimportance: should knowfreq 38%

basics

~10 s

It returns a brand-new Temporal.PlainDate and leaves the original untouched. To change one field, call with(), as in date.with({ month: 3 }), which also returns a new value. Temporal exposes no setters at all.

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 list of user-visible names sorted with names.sort() puts "Zebra" before "apple" and orders accented words oddly. Why does JavaScript's default string ordering do that, and what does Intl.Collator do differently?

level: middleimportance: should knowfreq 45%

basics

~20 s

The default comparison orders strings by UTF-16 code unit, so every uppercase letter precedes every lowercase one and accented letters land after "z". Intl.Collator compares by locale collation rules instead, which is what a human reader expects.

open as a page

A checkout page renders prices with '$' + amount.toFixed(2). What breaks once the product ships in more locales and currencies, and what does Intl.NumberFormat's style: 'currency' handle instead?

level: middleimportance: should knowfreq 50%

basics

~20 s

Hand-rolled currency hardcodes the symbol, its position, the separators and two decimal places. Intl.NumberFormat with style: 'currency' and a currency code derives all of them from locale and currency data — JPY gets zero decimals, German puts the symbol last.

open as a page

A UI builds a message as `${n} item${n === 1 ? '' : 's'}`. Explain why that pattern is an internationalization defect, and what Intl.PluralRules provides instead.

level: middleimportance: should knowfreq 28%

basics

~20 s

That pattern hardcodes English's two-form plural. Other languages have up to six categories with different rules. Intl.PluralRules.select(n) returns the category tag for a locale — 'one', 'few', 'many', 'other' — which you use to pick a message from a per-language catalog.

open as a page

What does a Temporal.Duration represent, and why does calling total({ unit: 'day' }) on a duration of one month throw unless you pass relativeTo?

level: middleimportance: should knowfreq 32%

basics

~20 s

A Temporal.Duration is a length of time held as a bag of unit fields, anchored to no point on the calendar. Years, months and weeks have no fixed length, so converting them to days needs a starting point, supplied as relativeTo — otherwise it throws a RangeError.

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

A service holds timestamps as epoch milliseconds and must display each one in a chosen time zone. How do you render that with Intl.DateTimeFormat, and why is slicing or regexing the formatted string to rearrange it a bug?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Pass a timeZone option with an IANA identifier to Intl.DateTimeFormat and format the timestamp; the zone affects only rendering. Never parse the output — component order, separators and digits vary by locale. Use formatToParts to assemble a custom layout from typed pieces.

open as a page

A meeting is booked for 09:00 local time in Berlin, eleven months from now. Using Temporal, why is persisting the computed UTC instant the wrong choice, and what would you store instead?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Time-zone rules change, so a UTC instant computed today can stop meaning 09:00 in Berlin. Store the wall-clock value and the IANA zone ID — a Temporal.PlainDateTime plus 'Europe/Berlin' — and resolve to an instant when you need it.

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

You own the formatting layer of a product shipped in twenty locales. How do you decide which locale each Intl formatter is constructed with, how do you verify what the runtime actually chose, and how do you keep formatting cheap on hot paths?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Pass an ordered preference list, not a single tag; negotiate it with supportedLocalesOf against the runtime's data; confirm the outcome with resolvedOptions().locale, which can differ from what you asked for; and cache formatter instances keyed by locale plus options, since construction is the expensive part.

open as a page

How would you introduce Temporal into a large existing JavaScript codebase that uses the built-in Date everywhere, given that Temporal is still a proposal and ships unevenly?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Adopt at boundaries rather than by sweep: convert Date to Temporal on the way in and back on the way out, move the highest-risk zone and calendar logic first, and pull Temporal from the official polyfill until engine support is broad.

open as a page