Parsing & formatting dates
- Predict which instant
new Date(string)builds from each ISO shape: date-only is UTC midnight, a date-time without an offset is local,Zor an offset is exact - Keep non-ISO strings (
"10/03/2026") away fromDateparsing, parse a known format by hand, and read<input type="date">values as date-only strings - Format for people with
Intl.DateTimeFormat(dateStyle/timeStyle,timeZone), and build machine strings withtoISOString()orformatToPartsinstead of slicing localized output - Write relative times with
Intl.RelativeTimeFormat, computing the value and unit yourself (numeric: "auto"gives "yesterday")
Read
Three ISO shapes, three different instants
Flagnew Date(string) reliably parses one family of strings: the ECMAScript date time string
format, a simplified ISO 8601 (YYYY-MM-DDTHH:mm:ss.sssZ). Inside that family, whether there is
an offset decides which instant you get:
// machine in America/New_York
new Date("2026-03-10"); // date-only → UTC midnight (2026-03-10T00:00Z)
new Date("2026-03-10T00:00"); // date-time, no offset → LOCAL midnight (2026-03-10T04:00Z)
new Date("2026-03-10T00:00Z"); // Z → exactly that (2026-03-10T00:00Z)
new Date("2026-03-10T00:00+05:30"); // offset → exactly that (2026-03-09T18:30Z)
The spec's rule: when the offset is absent, date-only forms are UTC and date-time forms are local. MDN calls the UTC reading of date-only strings "a historical spec error" that couldn't be fixed without breaking the web. It's why this prints the 9th in New York:
new Date("2026-03-10").getDate(); // 9: UTC midnight is 8 p.m. on March 9 in New York
Two rules follow:
- An instant on the wire always carries
Zor an offset.toISOString()produces exactly that, which is why it's the format for APIs, logs and sorting. - A date-only value is not an instant. Don't hand
"2026-03-10"tonew Dateand then read local getters. Keep it as a string, or do date math in UTC (the next unit is about exactly this).
Parsing what users and other systems actually send
FlagOutside the ISO family, the spec lets each engine "fall back to any implementation-specific
heuristics". So new Date("03/10/2026") is a guess, and the guess is American:
new Date("10/03/2026"); // V8 (Chrome, Node): October 3, 2026
// A UK user typed 10 March. Formatting can't fix a wrong parse.
MDN's advice is to make sure input conforms to the standard format, "because support for other formats is not guaranteed". When you know the format, parse it yourself, into numbers:
// A CSV from a UK client: DD/MM/YYYY
function parseUkDate(s) {
const m = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(s);
if (!m) throw new RangeError(`expected DD/MM/YYYY, got ${s}`);
const [, dd, mm, yyyy] = m;
return `${yyyy}-${mm}-${dd}`; // "2026-03-10": a date-only value, still unchecked
}
The regex only checks the shape: 31/02/2026 gets through. Checking that the date exists is
the next unit's job.
Form inputs already give you ISO strings
<input type="date">:valueis always"yyyy-mm-dd", whatever the display looks like (10.03.2026for a German user). That's a date-only string; keep it one. ItsvalueAsDateis aDateat midnight UTC (the HTML spec says so), soinput.valueAsDate.getDate()shows the previous day anywhere west of UTC.<input type="datetime-local">:valueis"YYYY-MM-DDTHH:mm", with no zone. In the browser,new Date(value)reads it as the user's local time, which is usually what they meant. Convert there and send the instant:
const instant = new Date(form.startsAt.value); // in the browser: the user's zone
await save({ startsAt: instant.toISOString() }); // New York user: "2026-03-10T13:30:00.000Z"
Sending the raw "2026-03-10T09:30" instead makes the server parse it in its zone. MDN also
notes the control has no way to set a zone. If the time belongs to a different place than the
user's device (an event in another city), collect the zone separately and convert with it.
new Date(value) never refuses a time, even one the clock skipped. On March 8, 2026,
"2026-03-08T02:30" doesn't exist in New York and becomes 03:30; on November 1, 01:30 happens
twice and you get the first. For a booking or a reminder, check that the instant still reads back
as the time the user picked, and ask when it doesn't. The date-only-and-dst unit covers both.
Strings that parse when they shouldn't, formatters that throw
FlagAn impossible date inside a valid shape. The format allows a day from 01 to 31 in any month,
so "2026-02-30" matches it. What happens next is up to the engine: MDN notes that for a string
whose components are in bounds but don't name a real date, like February 30, implementations
"behave inconsistently". V8 (Chrome, Node) rolls it forward:
new Date("2026-02-30").toISOString(); // V8: "2026-03-02T00:00:00.000Z"; other engines may give Invalid Date
new Date("2026-13-01").getTime(); // NaN everywhere: the spec requires NaN for an out-of-bounds month
So parsing succeeding doesn't mean the date exists. Validate the components yourself.
A space instead of T. "2026-03-10 09:00" is not in the standard format, so it's
implementation-defined. V8 reads it as local time, but don't rely on that. The same goes for
microseconds, two-digit offsets like +00 and other shapes databases and logs print: normalize
them to the standard format at the edge, or have the API send toISOString() output.
Two invalid-date behaviours. Formatting an Invalid Date is not consistent across APIs:
const bad = new Date("not a date");
bad.toLocaleDateString(); // "Invalid Date" (a string, no error)
new Intl.DateTimeFormat("en-US").format(bad); // throws RangeError: Invalid time value
The first renders "Invalid Date" in your UI; the second crashes the render. Both are cured by validating at the parse.
Formatter options that throw. The constructor validates its options:
new Intl.DateTimeFormat("en-US", { dateStyle: "medium", hour: "numeric" });
// TypeError: ECMA-402 forbids mixing a style preset with individual parts
new Intl.DateTimeFormat("en-US", { timeZone: "Mars/Olympus" });
// RangeError: invalid time zone (a typo in a stored user zone fails here)
A user's saved timeZone usually comes from the browser and is valid, but an import, an admin
form or a typo can put an unknown name there. Validate it when you save it (construct a
formatter once and catch the RangeError), not on every render.
Defaults come from the runtime. No locale means the runtime's default locale; no
timeZone means the runtime's zone. On a server that's the server's, not the user's (the
previous unit), and it's why the same toLocaleString() call renders differently in
development and production.
Quiz
On a machine in New York, what does new Date("2026-03-10").getDate() return?
Which string names the same instant on every machine?
FlagA CSV from a UK client contains 10/03/2026, meaning 10 March. How should you read it?
A German user picks 10 March 2026 in an <input type="date">, which displays 10.03.2026. What is input.value?
From an instant, you need a Tokyo user's calendar date as YYYY-MM-DD. Which code is right on any server?
What does new Intl.DateTimeFormat("en-US", { dateStyle: "medium", hour: "numeric" }) do?
new Intl.RelativeTimeFormat("en", { numeric: "auto" }).format(-1, "day") returns what, and where does the -1 come from?
Fill in the blank
Send an instant in the one format every machine parses to the same moment.
Flagconst body = JSON.stringify({ startsAt: startsAt.() });
Create one reusable formatter: a medium date and short time, on New York's wall clock.
Flagconst fmt = new Intl.DateTimeFormat("en-US", { : "medium", timeStyle: "short", : "America/New_York", }); fmt.format(new Date("2026-03-10T15:00:00Z")); // "Mar 10, 2026, 11:00 AM"
Pull the year out of formatted output without depending on the locale's order or punctuation.
Flagconst parts = fmt.(instant); const year = parts.find((p) => p. === "year").value;
Make the formatter say "yesterday" instead of "1 day ago".
Flagnew Intl.RelativeTimeFormat("en", { numeric: "" }).format(-1, "day"); // "yesterday"
Predict the output
// The machine is in America/New_York (UTC−4 on March 10, 2026).
console.log(new Date("2026-03-10").toISOString());
console.log(new Date("2026-03-10T00:00").toISOString());
console.log(new Date("2026-03-10T00:00Z").toISOString());// Node or Chrome (the V8 engine)
const d = new Date("2026-02-30");
console.log(d.toISOString());// Any machine, any zone.
const late = new Date("2026-03-10T23:30:00Z");
console.log(late.toISOString().slice(0, 10));
console.log(late.toLocaleDateString("en-US", { timeZone: "Asia/Tokyo" }));Further reading
- MDN: Date — date time string format
- MDN: Date.parse()
- ECMAScript spec: Date Time String Format
- MDN: Intl.DateTimeFormat() constructor
- MDN: Intl.DateTimeFormat.prototype.formatToParts()
- MDN: Date.prototype.toLocaleDateString()
- MDN: Intl.RelativeTimeFormat
- MDN: <input type="date">
- MDN: <input type="datetime-local">
- HTML spec: the date state (valueAsDate is midnight UTC)
- ECMA-402 spec: CreateDateTimeFormat (styles vs components)