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()`?
answer
- one number, no zone stored
- two getter families, local and UTC
- serialisation is always the UTC reading
- the offset method's sign is inverted
- offsets are not always whole hours
basics
~20 sA 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.
solid answer
~50 sA `Date` is a single time value — milliseconds since 1970-01-01T00:00:00Z — with no zone attached. Every field accessor is a projection of that number: `getHours()`, `getDate()` and `getMonth()` render it in whatever zone the host is configured for, while `getUTCHours()`, `getUTCDate()` and friends render the same instant in UTC. `toISOString()` and `toJSON()` always emit UTC, which is why a Date built at local midnight in UTC+02:00 serialises with the previous day's date: `getDate()` and `toISOString().slice(0, 10)` can legitimately disagree for the same object. `getTimezoneOffset()` is the sign trap — it returns UTC minus local time in minutes, so New York in winter gives +300 and Berlin in summer gives -120, the opposite of how offsets are written in ISO strings. It also varies with the date you call it on, because it reflects the DST rules in force at that instant.
code
javascript · 11 linesconst instant = new Date(Date.UTC(2025, 2, 15, 23, 30));
console.log(instant.getTime()); // same number everywhere
console.log(instant.toISOString()); // 2025-03-15T23:30:00.000Z
console.log(instant.getUTCDate()); // 15
console.log(instant.getUTCHours()); // 23
// The next two depend entirely on the host time zone:
console.log(instant.getDate());
console.log(instant.getHours());
console.log(instant.getTimezoneOffset()); // UTC minus local, in minutesgo deeper
Remember that a Date is just milliseconds since the epoch with no zone inside it, that the plain getters mean local time while the getUTC* ones mean UTC, and that toISOString() is always UTC.
Explain that every accessor is a projection of one number, state the getTimezoneOffset() sign convention (UTC minus local, so negative east of Greenwich), and show why the local day and the ISO day can differ for the same object.
Demonstrate how host time zone leaks into tests and stored data, and describe the boundary discipline you enforce: instants in UTC through storage and transport, local rendering only at the display edge.
Own the consequences of Date offering only two projections — host-local and UTC. Decide where zone conversion is allowed to live, and what the system stores when a value is a calendar date rather than an instant.
## One number, many readings Internally a `Date` is a *time value*: an integral number of milliseconds since the epoch, 1970-01-01T00:00:00Z. `getTime()` and `valueOf()` hand it to you unchanged, and `Date.now()` produces one without allocating an object. There is no time-zone field anywhere in the object — that is the single fact from which every behaviour below follows. Because there is no stored zone, a Date cannot answer "what time is it?" without being told which zone to render in. The API offers exactly two answers, and they are baked into the method names. ## The two getter families ```js const d = new Date('2025-03-15T23:30:00Z'); // host time zone: UTC+02:00 d.getHours(); // 1 <- local d.getUTCHours(); // 23 <- UTC d.getDate(); // 16 <- local d.getUTCDate(); // 15 <- UTC ``` Every calendar accessor comes in a pair: `getFullYear`/`getUTCFullYear`, `getMonth`/`getUTCMonth`, `getDate`/`getUTCDate`, `getDay`/`getUTCDay`, `getHours`/`getUTCHours`, and so on down to milliseconds. The unprefixed one is *local*, meaning the zone the host process is configured with — the operating system setting in a browser, the `TZ` environment variable or system zone in a server process. The `UTC` one is fixed. Setters mirror this exactly: `setHours()` writes a local wall-clock hour, `setUTCHours()` writes a UTC one. Mixing families on the same object is a reliable way to produce a date nobody expects. Note that `getTime()`, `valueOf()` and the milliseconds are zone-independent — the instant itself does not care how you read it. ## Serialisation is always UTC `toISOString()` produces the ISO 8601 form in UTC with a trailing `Z`, and `toJSON()` simply calls it, which is what `JSON.stringify()` uses. So the serialised form of a Date is always the UTC reading, no matter which zone built it. ```js const localMidnight = new Date(2025, 2, 16); // host at UTC+02:00 localMidnight.getDate(); // 16 localMidnight.toISOString(); // '2025-03-15T22:00:00.000Z' localMidnight.toISOString().slice(0, 10); // '2025-03-15' <-- off by one ``` That last line is the single most common date bug in JavaScript codebases: taking the date part of an ISO string to get "the day" of a locally-constructed Date. Both readings are correct; they are just readings of one instant in two different zones. If you want the local calendar day as a string, build it from local getters, not from `toISOString()`. By contrast, `toString()` renders local time with the offset and zone name, `toUTCString()` renders UTC in the older HTTP-style format, and `toDateString()`/`toTimeString()` give the local date and time halves. ## getTimezoneOffset() and its inverted sign `getTimezoneOffset()` returns **UTC time minus local time, in minutes**. Written as a formula: `utcFields - localFields`. So: - New York in January (UTC-05:00) returns `300`. - Berlin in July (UTC+02:00) returns `-120`. - London in January (UTC+00:00) returns `0`. The sign is inverted relative to how the same offset is written in an ISO string (`-05:00`, `+02:00`), which is exactly why hand-rolled conversions get it backwards. The reliable mental model: the number tells you how many minutes you must *add* to local time to reach UTC. Two further points. First, it is a method on an instance, not a static: it reports the offset *in force at that instant*, so calling it on a January date and a July date in a DST-observing zone gives different answers. Second, the value can be a non-multiple of 60 — India is UTC+05:30, so it returns -330 — which breaks any code that divides by 60 and assumes an integer. ```js function offsetSuffix(d) { const total = -d.getTimezoneOffset(); // flip to ISO sign const sign = total >= 0 ? '+' : '-'; const abs = Math.abs(total); const hh = String(Math.floor(abs / 60)).padStart(2, '0'); const mm = String(abs % 60).padStart(2, '0'); return `${sign}${hh}:${mm}`; } ``` ## What this implies for design The Date object gives you precisely two projections of an instant: the host's current zone and UTC. It does not model "the time in Tokyo" as a value, and it cannot store a wall-clock time that is independent of a zone. Consequently the durable rule is: **store and transmit instants in UTC, convert only at the display edge**, and keep values that are genuinely calendar dates (a birthday, a billing period) out of Date entirely. A practical corollary for tests: because the unprefixed getters depend on the host, any assertion written against them is a test of your CI machine's zone as much as of your code. Either pin the zone the test runs in, or assert on `getTime()` and the UTC getters.
- Why do getDate() and toISOString().slice(0, 10) sometimes disagree for the same Date object?Because they read the same instant in different zones. `getDate()` projects into the host's local zone; `toISOString()` always projects into UTC. A Date built at local midnight in a positive-offset zone is still on the previous UTC day, so the sliced string is one day behind. To get the local day as text, assemble it from getFullYear/getMonth/getDate instead.
- What does getTimezoneOffset() return in a zone that is UTC+05:30, and what does that break?It returns -330. Two things break: code that assumes the sign matches the ISO notation (+05:30) inverts the conversion, and code that divides by 60 expecting whole hours drops the half-hour. India, Newfoundland at -03:30, and Nepal at +05:45 all fail whole-hour arithmetic.
- How would you make a unit test that asserts on local date fields deterministic?Pin the process time zone — set TZ for the test run — or avoid local getters in assertions entirely and assert on getTime() or the getUTC* family. Otherwise the test encodes the machine's zone, and it passes in a UTC container while failing on a developer laptop in Los Angeles.
saying these in an interview costs you the question
- Thinks a Date stores the time zone it was created in
- Says getTimezoneOffset() returns +120 for UTC+02:00
- Uses toISOString().slice(0,10) for the local calendar day
- Assumes every time-zone offset is a whole number of hours
- Believes getTimezoneOffset() is constant for a given machine