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?
answer
- one format is specified, the rest is not
- date-only versus date-time differ
- the missing offset decides the zone
- off-by-one day west of Greenwich
- invalid parse gives NaN, not a throw
basics
~20 sA 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.
solid answer
~40 sECMAScript defines one string format that every engine must parse: the simplified ISO 8601 Date Time String Format. Within it there is a deliberate split — a date-only form such as `'2025-03-15'` is interpreted as UTC, while a date-time form with no offset such as `'2025-03-15T00:00:00'` is interpreted as local time. So on a machine at UTC-05:00 the first is 2025-03-15T00:00:00Z and `getDate()` returns 14, while the second is 2025-03-15T05:00:00Z and `getDate()` returns 15. Anything outside that format — `'03/15/2025'`, `'March 15, 2025'`, `'2025-3-15'` — is explicitly implementation-defined: engines may parse it, parse it differently, or return an Invalid Date, so never rely on it. The safe rule is to accept only strings carrying an explicit offset or `Z`, or to parse the fields yourself and build the Date with `Date.UTC()`.
code
javascript · 11 lines// Run on a machine whose time zone is UTC-05:00.
const dateOnly = new Date('2025-03-15');
const dateTime = new Date('2025-03-15T00:00:00');
console.log(dateOnly.toISOString()); // 2025-03-15T00:00:00.000Z
console.log(dateOnly.getDate()); // 14
console.log(dateTime.toISOString()); // 2025-03-15T05:00:00.000Z
console.log(dateTime.getDate()); // 15
console.log(dateOnly.getTime() === dateTime.getTime()); // falsego deeper
Know that a bare date string like '2025-03-15' becomes UTC midnight, so local getters can report the previous day, and that a failed parse gives an Invalid Date rather than an exception.
Explain the specification's split — date-only is UTC, date-time without an offset is local, an explicit offset settles it — and say that any other string format is implementation-defined and must not be relied on.
Show how this bug hides in CI running in UTC and appears only for users west of Greenwich, and describe the input contract you would enforce: explicit offsets on the wire, structured fields from the UI, validation right at the parse site.
Own the representation decision: which values in the system are instants and which are calendar dates, so date-only data never round-trips through an instant type and no layer has to guess what a bare timestamp meant.
## Two parsers in one function `Date.parse()` — which `new Date(string)` delegates to — has two layers. The first is the *Date Time String Format* defined in the ECMAScript specification, a simplified profile of ISO 8601 that every conforming engine must accept identically. The second is a fallback: "if the string does not conform, the function may fall back to any implementation-specific heuristics". That fallback is where portability dies. ## The specified format The format is `YYYY-MM-DDTHH:mm:ss.sssZ`, where trailing components may be dropped. Legal examples include `2025`, `2025-03`, `2025-03-15`, `2025-03-15T14:30`, `2025-03-15T14:30:00.000Z`, `2025-03-15T14:30:00+02:00`. The interpretation rule is the part interviewers probe: - **Date-only forms** (`2025`, `2025-03`, `2025-03-15`) are treated as **UTC**. - **Date-time forms with an offset** (`...Z`, `...+02:00`) mean exactly what the offset says. - **Date-time forms without an offset** (`2025-03-15T00:00:00`) are treated as **local time**. ```js // machine set to UTC-05:00 new Date('2025-03-15').toISOString(); // 2025-03-15T00:00:00.000Z new Date('2025-03-15').getDate(); // 14 <-- the off-by-one new Date('2025-03-15T00:00:00').toISOString(); // 2025-03-15T05:00:00.000Z new Date('2025-03-15T00:00:00').getDate(); // 15 ``` That asymmetry is not an accident; it is a documented compromise. ES5 specified date-only strings as UTC, ES6 briefly made everything without an offset local, and the web broke, so ES2016 settled on the current split and it is now permanent. ## Why the off-by-one bug is so common A date-only value — a birthday, an invoice date, a due date — has no instant attached to it. Parsing it produces UTC midnight, and every local getter on a machine west of Greenwich then reports the day before. It survives all your tests if your team and CI both run in UTC or in a zone east of Greenwich, and it appears the moment a user in the Americas loads the page. The mirror-image bug happens when you *write* a date-only value: `d.toISOString().slice(0, 10)` on a local-midnight Date in UTC+02:00 also shifts the day, because `toISOString()` converts to UTC first. The structural fix is to stop round-tripping calendar dates through an instant type at all: keep `'2025-03-15'` as a string, or as a `{year, month, day}` record, and only build a Date when you genuinely need an instant. ## Everything outside the format Strings the specification does not cover are implementation-defined. Concretely: - `'2025-3-15'` — not zero-padded, so not the specified format. Some engines parse it as local time; the string is not portable. - `'03/15/2025'` — widely accepted in browsers as a local-time US-order date; there is no guarantee, and `'15/03/2025'` may be rejected or misread. - `'March 15, 2025'` — commonly accepted, still implementation-defined, and locale-dependent in what it will tolerate. - `'2025-03-15 14:30'` — a space instead of `T`. Historically rejected by some engines, accepted by others; recent editions of the specification permit a space separator in the fallback but it remains outside the guaranteed format. When parsing fails the result is an *Invalid Date*: a real Date object whose time value is `NaN`. ```js const d = new Date('not a date'); d instanceof Date; // true Number.isNaN(d.getTime());// true <-- the correct check d.toString(); // 'Invalid Date' d.toISOString(); // throws RangeError ``` Note that the object is truthy and passes `instanceof`, so a guard like `if (d)` catches nothing; and that `toISOString()` throws rather than returning a marker, which is how a bad parse three layers up often surfaces as a `RangeError` in a serialisation step. ## Practical rules 1. **On input, require an explicit offset.** A trusted API should emit `2025-03-15T14:30:00Z` or `...+02:00`; then no interpretation rule matters. 2. **Never feed user-typed dates to `Date.parse`.** Take structured fields from a date picker, or parse the string yourself with a regular expression and construct with `Date.UTC()`. 3. **Treat date-only values as dates, not instants.** If the value means "15 March" rather than "a moment", it should never become a Date whose reading depends on the reader's zone. 4. **Always check the result.** `Number.isNaN(d.getTime())` immediately after parsing; do not wait for `toISOString()` to throw. 5. **`Date.parse` returns a number, not a Date.** It yields a time value or `NaN`, which makes the validity check a plain `Number.isNaN` on the return value.
- How do you correctly detect that a parse failed?Check the time value: `Number.isNaN(d.getTime())`, or use `Date.parse(s)` directly and test its numeric result. An Invalid Date is still a real object — it is truthy and passes `instanceof Date` — so a plain truthiness guard is useless. Waiting for `toISOString()` to throw a RangeError works but surfaces the failure far from its cause.
- A backend sends the string '2025-03-15' meaning a due date. What is the safest way to display it as 15 March for every user?Do not turn it into a Date at all — split the string, or keep it as year/month/day fields. If you must build a Date for a formatting API, construct it with the local constructor (`new Date(2025, 2, 15)`) so local getters agree with the stored value; parsing it directly gives UTC midnight, which reads as 14 March anywhere west of Greenwich.
- Why is '2025-03-15T14:30:00' without an offset considered ambiguous data rather than just a local timestamp?Because the instant it denotes depends on whichever machine parses it. A browser in Berlin and a server in Chicago produce time values seven hours apart from identical bytes, so the value is not self-describing. Any timestamp crossing a process boundary should carry `Z` or an explicit offset.
saying these in an interview costs you the question
- Believes every date string parses the same in all engines
- Thinks '2025-03-15' parses as local midnight
- Uses new Date('03/15/2025') as if it were portable
- Checks a failed parse with if (date) instead of NaN
- Assumes an unparseable string makes the constructor throw