How to Parse a Date in JavaScript
To parse a date in JavaScript, pass new Date() an ISO 8601 string with an offset, such as '2026-03-04T10:00:00Z'. That is the only format every engine must read the same way. For any other format (05/20/2026, March 4, 2026, user input), use a library that takes an explicit format string, such as date-fns or Luxon. Where it ships, the built-in Temporal API does both jobs.
Why date parsing in JavaScript is hard

JavaScript’s Date is hard to use because one object stands for a date, a time and a moment, and it guesses the timezone when you don’t give one. It was copied from Java’s java.util.Date and kept that design’s flaws for compatibility.
Developers notice. In the State of JavaScript 2025 survey (13,002 responses, results published February 3, 2026), date management was the second most-named language pain point, after the lack of static typing.
The timezone trap
A string with no offset is read differently depending on its shape. A date-only string like '2026-03-04' is read as UTC, while a date-time string like '2026-03-04T10:00' is read as local time (MDN). The spec keeps that inconsistency for web compatibility.
const myDate = new Date('2026-03-04');
This is midnight on March 4 in UTC, the same moment everywhere. The surprise comes when you display it: myDate.getDate() and myDate.toLocaleDateString() use the local timezone, so a user in New York (UTC-5) sees March 3, 7 p.m., while a teammate in London sees March 4. Add a time, as in new Date('2026-03-04T00:00'), and the same code now means local midnight, a different moment on every machine. Most off-by-one-day bugs start here.
Other Date gotchas
- Zero-based months. January is
0and December is11, sonew Date(2026, 3, 4)is April 4, not March 4. - Setters mutate.
setDate()andsetMonth()change the object in place instead of returning a new one, so a date you passed to another function can change under you. - Silent overflow. An impossible date rolls over instead of throwing. Start from January 31, 2026, call
setMonth(1)(February), and you get March 3, because February 31 doesn’t exist.
Parsing with the native Date object
new Date(string) and Date.parse(string) use the same parser. The constructor returns a Date object; Date.parse() returns milliseconds since the Unix epoch, or NaN if it can’t read the string.
// The constructor returns a Date object
const date = new Date('2026-07-15T10:00:00Z');
console.log(date.toUTCString());
// Wed, 15 Jul 2026 10:00:00 GMT
// Date.parse returns a timestamp in milliseconds
const timestamp = Date.parse('2026-07-15T10:00:00Z');
console.log(timestamp);
// 1784109600000
A string the parser can’t read gives an Invalid Date rather than an error. Check with Number.isNaN(date.getTime()) before you use the result.
Use ISO 8601 with an offset
Pass the ECMAScript date time string format, a simplified ISO 8601 that toISOString() produces, and include an offset (Z or +02:00). It is the only input format engines are required to support. The output of toString() and toUTCString() also parses back; every other format is implementation-defined.
The full form is YYYY-MM-DDTHH:mm:ss.sssZ:
YYYY-MM-DD: the date, like2026-10-26.T: the separator between date and time.HH:mm:ss.sss: the time, like13:30:00.000.Z: the offset.Zmeans UTC;+02:00means two hours ahead of UTC.
Avoid ambiguous strings
Any other format is a guess, and engines don’t have to guess the same way:
'03/04/2026': March 4 in the US, April 3 in most of Europe. The parser ignores the user’s locale. Engines commonly read it month first, so a European user’s April 3 silently becomes March 4.'March 4, 2026': most engines read it, but the spec doesn’t require them to, and small variations returnInvalid Date.
The spec calls the behavior for these strings “implementation-defined”: Chrome, Firefox and Safari may each read the same string differently. If you don’t control the input and can’t guarantee ISO 8601, don’t hand it to Date.
Parsing with date-fns, Luxon or Day.js
A date library parses with a format string you supply, so the same input gives the same result in every browser. The three common choices are date-fns, Luxon and Day.js. Each example below parses '05/20/2026 10:30 AM'; the sample outputs are from a machine in New York.
date-fns
date-fns is a set of functions you import one at a time, which keeps bundles small. Its functions never modify a Date you pass in; they return a new one.
To parse a custom string, call parse with the string, a format pattern and a reference date, which fills in any parts the pattern leaves out:
import { parse, isValid } from 'date-fns';
const parsedDate = parse('05/20/2026 10:30 AM', 'MM/dd/yyyy hh:mm a', new Date());
console.log(isValid(parsedDate)); // true
console.log(parsedDate.toString());
// Wed May 20 2026 10:30:00 GMT-0400 (Eastern Daylight Time)
If the string doesn’t match the pattern, parse returns an Invalid Date and isValid returns false.
Luxon
Luxon lives under the Moment.js project as a modern replacement for Moment. Its DateTime objects are immutable, and it supports IANA timezones such as America/New_York without plugins.
import { DateTime } from 'luxon';
const dt = DateTime.fromFormat('05/20/2026 10:30 AM', 'MM/dd/yyyy hh:mm a');
console.log(dt.isValid); // true
console.log(dt.toString()); // 2026-05-20T10:30:00.000-04:00
When parsing fails, dt.isValid is false and dt.invalidReason says why.
Day.js
Day.js calls itself a “fast 2kB alternative to Moment.js with the same modern API”, so it can often replace Moment with few code changes. Its objects are immutable. Custom formats and timezones come from plugins, so extend it with customParseFormat first. Note that its tokens differ from date-fns: DD, YYYY and A instead of dd, yyyy and a.
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
dayjs.extend(customParseFormat);
const d = dayjs('05/20/2026 10:30 AM', 'MM/DD/YYYY hh:mm A');
console.log(d.isValid()); // true
console.log(d.format()); // 2026-05-20T10:30:00-04:00
Which library to pick
| Feature | date-fns | Luxon | Day.js |
|---|---|---|---|
| API style | Functions: parse(str, fmt, ref) |
Objects: DateTime.fromFormat() |
Moment-style: dayjs(str, fmt) |
| Immutable | Yes | Yes | Yes |
| Size | Small: you import only what you use | Larger: one bundle | Small core (~2 kB) plus plugins |
| Timezones | Separate @date-fns/tz package |
Built in (IANA names) | utc and timezone plugins |
| Best for | Small bundles, functional code | Apps that work across timezones | Moving off Moment.js |
All three parse reliably. Pick date-fns for bundle size, Luxon for timezone-heavy apps, and Day.js if you are replacing Moment.js.
The Temporal API
Temporal is the built-in replacement for Date. It reached Stage 4 at TC39 in March 2026 and is expected in the ES2027 edition of the standard (TC39 finished proposals).
As of October 2026 it ships in Firefox 139+, Chrome and Edge 144+, Node.js 26, Deno 2.7 and Bun 1.4. Safari has it only in Technology Preview (MDN browser compatibility data). For Safari users, load a polyfill such as @js-temporal/polyfill (import { Temporal } from '@js-temporal/polyfill') until it ships.
What Temporal fixes
- Immutable objects. Adding a day or changing a timezone returns a new object; the original is untouched.
- Explicit timezones. “Plain” types have no timezone and
ZonedDateTimecarries one. Parsing never assumes the machine’s local zone. - One type per job. A date, a time, a date-time and an exact instant are separate types, so a
PlainDatecan’t carry a hidden time that throws off date math. - Months start at 1. October is
10.
The main Temporal types
Temporal.PlainDate: a calendar date with no time or timezone, such as2026-10-26. Use it for birthdays and due dates.Temporal.PlainTime: a time of day with no date or timezone, such as14:30:00.Temporal.PlainDateTime: a date and time with no timezone.Temporal.ZonedDateTime: an exact moment in a named timezone. Use it for meetings and events.Temporal.Instant: an exact moment with no timezone, like a Unix timestamp with nanosecond precision.
Parsing a string with Temporal
Each type has a from() method that parses ISO 8601 strings and throws a RangeError on anything else:
const myDate = Temporal.PlainDate.from('2026-10-26');
console.log(myDate.year); // 2026
console.log(myDate.month); // 10 (months start at 1)
console.log(myDate.day); // 26
For a date-time in a timezone, use ZonedDateTime with the IANA name in brackets:
const meetingTime = Temporal.ZonedDateTime.from('2026-11-05T09:00:00[America/New_York]');
console.log(meetingTime.timeZoneId); // America/New_York
console.log(meetingTime.hour); // 9
Temporal makes the timezone explicit: the code says which zone it means, and the result doesn’t depend on the machine it runs on.
Handling timezones from parsing to display

Most date bugs are timezone bugs. Attach a timezone when you parse, store a UTC moment, and convert to the user’s timezone only when you display it.
Use IANA timezone names, not offsets:
- A UTC offset such as
-05:00is a fixed number. It knows nothing about daylight saving time: New York is-05:00in January and-04:00in July. - An IANA timezone such as
America/New_Yorknames a region and carries its history of rule changes, including daylight saving time.
Parsing in a specific timezone
“November 5, 2026 at 10:00 AM” isn’t a moment until you know where. Luxon takes a zone option when it parses:
import { DateTime } from 'luxon';
const eventTime = DateTime.fromFormat('2026-11-05 10:00', 'yyyy-MM-dd HH:mm', {
zone: 'America/New_York',
});
console.log(eventTime.zoneName); // America/New_York
console.log(eventTime.toString()); // 2026-11-05T10:00:00.000-05:00
Temporal parses a plain date-time, then attaches the zone:
const plainDateTime = Temporal.PlainDateTime.from('2026-11-05T10:00:00');
const eventTime = plainDateTime.toZonedDateTime('America/New_York');
console.log(eventTime.toString()); // 2026-11-05T10:00:00-05:00[America/New_York]
Converting between timezones
Once a value has a zone, converting it is one call. To show the New York event to a user in Berlin with Luxon, call setZone() on the Luxon eventTime above:
const berlinTime = eventTime.setZone('Europe/Berlin');
console.log(berlinTime.toString()); // 2026-11-05T16:00:00.000+01:00
With Temporal, call withTimeZone() on the Temporal eventTime:
const berlinTime = eventTime.withTimeZone('Europe/Berlin');
console.log(berlinTime.toString()); // 2026-11-05T16:00:00+01:00[Europe/Berlin]
The moment is the same; only the wall-clock time changes, from 10:00 to 16:00.
Formatting for the user’s locale
Format dates for display with Intl.DateTimeFormat, not a hardcoded pattern like MM/DD/YYYY, which confuses anyone outside the US. It applies the locale’s own order, names and separators. Pass the timeZone option too, or it uses the machine’s timezone. With Luxon, format the native Date from toJSDate():
const formatter = new Intl.DateTimeFormat('de-DE', {
dateStyle: 'full',
timeStyle: 'long',
timeZone: 'Europe/Berlin',
});
console.log(formatter.format(eventTime.toJSDate()));
// Donnerstag, 5. November 2026 um 16:00:00 MEZ
A Temporal ZonedDateTime formats in its own timezone with toLocaleString():
console.log(berlinTime.toLocaleString('de-DE', { dateStyle: 'full', timeStyle: 'long' }));
// Donnerstag, 5. November 2026 um 16:00:00 MEZ
The full workflow: parse with an explicit timezone, store a UTC timestamp or an ISO string ending in Z, and format in the user’s timezone and locale.
Frequently asked questions
What’s the safest way to parse a date string in JavaScript?
Pass new Date() an ISO 8601 string with an offset, such as '2026-10-26T10:00:00Z'. It’s the only format every engine must parse the same way. For any other format, such as '10/26/2026', use date-fns, Luxon or Day.js with an explicit format string, or Temporal where it ships.
How should I handle timezones when parsing dates?
Don’t let the parser guess. Have your backend send ISO 8601 strings in UTC (ending in Z), and when you must parse a local time, attach an IANA timezone such as America/New_York with Luxon’s zone option or Temporal’s toZonedDateTime(). Use IANA names rather than fixed offsets so daylight saving time is handled for you.
Why does new Date(‘2026-03-04’) show the day before?
A date-only ISO string is parsed as midnight UTC. West of UTC, local display methods such as getDate() and toLocaleDateString() show the previous evening, March 3. Parse a full date-time with an offset instead, or keep calendar dates as Temporal.PlainDate, which has no timezone.
Can I use the Temporal API now?
As of October 2026, yes in Chrome, Edge, Firefox, Node.js 26, Deno and Bun, where Temporal is built in. Safari has it only in Technology Preview, so a web app that supports Safari needs a polyfill such as @js-temporal/polyfill. If the polyfill’s size is a problem, date-fns or Luxon are still good choices.