JAVASCRIPT / DATES, TIME, AND INTERNATIONALIZATION
Parsing date strings without surprises
Predict exactly which instant a date string parses to, tell the UTC-versus-local forms apart, and hand-parse non-ISO formats safely.
What you will learn
- Read a bare '2024-03-05' as UTC midnight, not as local midnight.
- Know that '2024-03-05T00:00:00' with no offset means local time.
- Hand-parse non-ISO formats with a regex and build them with Date.UTC.
- Check Number.isNaN(d.getTime()) instead of truthiness to detect a failed parse.
Understanding Parsing date strings without surprises
new Date(someString) and Date.parse(someString) run the same operation, and that operation is two parsers stacked. First the engine tries the format the language actually specifies, a subset of ISO 8601: YYYY-MM-DD, optionally followed by THH:mm, THH:mm:ss or THH:mm:ss.sss, optionally followed by Z or an offset like +05:30. If the string does not match, the engine may do whatever it likes, which is why V8 in Chrome and Node reads '05/03/2024' as 3 May while another engine is free to reject it. Portable code relies only on the first parser.
Inside the specified format there is one asymmetry worth memorising: when the string carries no offset, the engine fills one in, and its choice depends on whether a time is present. Date-only strings are treated as UTC; date-time strings are treated as local time. So '2024-03-05' is midnight UTC while '2024-03-05T00:00:00' is midnight wherever the code happens to run, and they are the same instant only in a UTC zone. That is the source of the classic off-by-one-day bug: getDate() on the UTC-midnight value reports 5 in Berlin but 4 in Sao Paulo, because there the instant is still 21:00 on the 4th.
A failed parse does not throw. You get a real Date object whose internal time value is NaN: it prints as 'Invalid Date', it is truthy, and it stays quiet until something forces the number out, at which point toISOString throws a RangeError or a total renders as NaN far from the bad input. So treat every incoming string as a shape to validate: test it with a regex, read the numbers yourself, build a calendar day with Date.UTC(year, month - 1, day) or an instant from an offset-bearing string, and round-trip the result to reject impossible dates like 31/02 that Date.UTC silently rolls forward.
const dateOnly = new Date('2024-03-05');
const localMidnight = new Date('2024-03-05T00:00:00');
// A date-only string is parsed as UTC midnight, in every time zone.
console.log(dateOnly.toISOString());
// With a time but no offset it is local midnight instead, so the two
// instants sit exactly one local offset apart.
const gapMinutes = (localMidnight.getTime() - dateOnly.getTime()) / 60000;
console.log(gapMinutes === localMidnight.getTimezoneOffset());
// Spell the offset out and the instant stops depending on the reader.
console.log(new Date('2024-03-05T00:00:00+05:30').toISOString());Whether a date string names one exact instant depends on whether it carries an offset, and anything outside the specified ISO subset is up to the engine.
Worked examples
Strict day-first parsing
Parsing a dd/mm/yyyy string by hand so the field order and the invalid days are both under your control.
function parseDMY(text) {
const m = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(text);
if (!m) return null;
const day = Number(m[1]);
const month = Number(m[2]);
const year = Number(m[3]);
const d = new Date(Date.UTC(year, month - 1, day));
if (d.getUTCMonth() !== month - 1 || d.getUTCDate() !== day) return null;
return d;
}
console.log(parseDMY('05/03/2024').toISOString());
console.log(parseDMY('31/02/2024'));
console.log(parseDMY('2024-03-05'));Example explained
Line 1The regex pins the field order, so '05/03/2024' can only mean 5 March; new Date in Chrome or Node would read it as 3 May.
Line 2Date.UTC takes a 0-based month, hence month - 1, and returns a time value rather than a Date object.
Line 3Date.UTC(2024, 1, 31) rolls forward to 2 March instead of failing, so the getUTCMonth/getUTCDate round-trip is what rejects 31/02.
Line 4A string that does not match returns null instead of falling through to the engine's own fallback parser.
Offsets make an instant unambiguous
Two strings with different wall-clock times and explicit offsets parse to the identical time value.
const utc = new Date('2024-03-05T09:30:00Z');
const india = new Date('2024-03-05T15:00:00+05:30');
console.log(utc.getTime() === india.getTime());
console.log(india.toISOString());
console.log(new Date('2024-03-05T09:30:00.250Z').getUTCMilliseconds());
console.log(Date.parse('2024-03-05T09:30Z') === Date.parse('2024-03-05T09:30:00Z'));Example explained
Line 115:00 at +05:30 is 09:30 UTC, so both strings resolve to the same number of milliseconds since the epoch.
Line 2toISOString always reports UTC, which is why the +05:30 string comes back out as 09:30Z.
Line 3The specified format includes exactly three fractional digits, so .250 is parsed as 250 milliseconds.
Line 4Seconds are optional in that format, so '09:30Z' and '09:30:00Z' give equal time values instead of one of them falling back to engine-specific parsing.
Invalid Date fails quietly
What an unparseable string actually produces, and why null is worse than a bad string.
const bad = new Date('qwe');
console.log(String(bad));
console.log(Boolean(bad), Number.isNaN(bad.getTime()));
try {
bad.toISOString();
} catch (err) {
console.log(err.name);
}
console.log(new Date(null).toISOString());Example explained
Line 1The constructor does not throw on garbage: it returns a Date whose time value is NaN and whose string form is 'Invalid Date'.
Line 2That object is truthy, so if (bad) passes; getTime() returning NaN is the check that actually works.
Line 3toISOString is usually where the NaN finally surfaces, as a RangeError raised far away from the bad input.
Line 4new Date(null) is not a parse at all: null converts to the number 0, so a missing field turns into the epoch instead of an error.
Important notes
A trailing Z is not decoration. '2024-03-05T00:00:00' and '2024-03-05T00:00:00Z' can be hours apart and can fall on different calendar days.
Unpadded or shortened strings such as '2024-3-5' and '5/3/24' are outside the specified format entirely, so any success you see there is one engine's behaviour, not a guarantee.
Common mistakes
Calling getDate() on new Date('2024-03-05') and expecting 5: west of UTC the local instant is still the 4th, so every user in the Americas sees the day before the one stored.
Passing a day-first string like '05/03/2024' straight to new Date: Chrome and Node interpret it month-first and silently return 3 May, while another engine may return Invalid Date, so the bug only shows up in some browsers.
Validating with if (d) or d !== 'Invalid Date': an Invalid Date object is truthy and is not a string, so the NaN survives until toISOString throws or a UI prints 'Invalid Date'.
Try it yourself
Change, predict, then run
In a browser console, log Date.parse('2024-03-05T12:00:00') - Date.parse('2024-03-05T12:00:00Z') and check it matches your zone's offset for that day in milliseconds. Then write strictDay(s) that returns a Date only for a zero-padded YYYY-MM-DD string and null for '2024-3-5' and '31/02/2024'.
Open the JavaScript workspaceCheck your understanding
An API returns the birthday '1990-07-04'. The page renders it with new Date('1990-07-04').getDate() and a user in Sao Paulo (UTC-3) sees 3. Why?
- getDate() is 0-based, like getMonth(), so it reports one less than the day in the string.
- The engine used its legacy fallback parser, which assumes month-first order.
- The date-only string was parsed as UTC midnight, and getDate() reports that instant in local time, where it is still 3 July.
- A string with no time part is stored as 23:00 on the previous day.
Show answer
'1990-07-04' is a date-only string, so it parses to midnight UTC; getDate() converts that instant to the reader's zone, and at UTC-3 midnight UTC is 21:00 on 3 July. Option 0 is the tempting one, but getDate() returns 1 to 31 and only getMonth() is 0-based; the fallback parser never runs here because the string matches the specified format. Use getUTCDate(), or read the fields out of the string, to render a stored calendar day.