JavaScript reference · Dates & times
JavaScript Date & Time Handling
The JavaScript answer to “how do I do dates?” in one place — the built-in
Date and why it hurts, the modern Temporal API, formatting with
Intl.DateTimeFormat, parsing, time zones, UTC, Unix milliseconds, and the foot-guns.
Which do I reach for?
Temporal
The modern default — immutable, time-zone-aware, unambiguous. Temporal.Instant / ZonedDateTime for a moment, PlainDate/PlainTime for a wall value. Standard (ES2027) and in current engines; polyfill for Safari.
Date
The built-in. Fine for a simple epoch or an ISO round-trip, but mutable, 0-indexed on months, compares by reference, and has no real time-zone support. The source of most JS date bugs.
Intl.DateTimeFormat
The native formatter — how you turn an instant into human text in any locale and any IANA zone (an options object, not a format string). Not a date type; it works on a Date or a Temporal value.
A library
Reach for Luxon or date-fns when you need format tokens or the widest legacy reach today; Day.js for a tiny Moment-compatible API. Moment is legacy — its own maintainers steer new projects elsewhere.
Formatting reference
The most-searched date answer: “how do I format the date?” JavaScript has
no native format-string language — there is no
date.toString("yyyy-MM-dd"). The native answer is
Intl.DateTimeFormat / toLocaleString with an
options object (below), plus toISOString()
for machine output. Token strings like "yyyy-MM-dd" exist only in
libraries, and they disagree with each other — the second table maps them.
0 of 0 rows
Example output is the canonical instant — Monday, June 15, 2009 1:45:30.617 PM at offset -07:00 (i.e. 2009-06-15T20:45:30.617Z) — formatted with locale en-US and timeZone: 'America/Los_Angeles', so the wall clock reads 1:45:30 PM. toLocaleString output is locale- and engine-dependent.
Native — Intl.DateTimeFormat options
| Option | Values | Example (en-US) |
|---|---|---|
| weekday | 'long' / 'short' / 'narrow' | Monday / Mon / M |
| year | 'numeric' / '2-digit' | 2009 / 09 |
| month | 'long' / 'short' / 'narrow' / 'numeric' / '2-digit' | June / Jun / J / 6 / 06 |
| day | 'numeric' / '2-digit' | 15 / 15 |
| hour | 'numeric' / '2-digit' (with hour12) | 1 PM / 01 PM · 13 (24h) |
| minute | 'numeric' / '2-digit' | 45 |
| second | 'numeric' / '2-digit' | 30 |
| fractionalSecondDigits | 1 / 2 / 3 | 617 |
| dayPeriod | 'narrow' / 'short' / 'long' | in the afternoon |
| hour12 | true / false | 1 PM (true) · 13 (false) |
| timeZone | an IANA id, e.g. 'America/Los_Angeles', 'UTC' | formats in that zone |
| timeZoneName | 'short' / 'long' / 'shortOffset' / 'longOffset' | PDT / Pacific Daylight Time / GMT-7 / GMT-07:00 |
| era | 'narrow' / 'short' / 'long' | AD |
| dateStyle | 'full' / 'long' / 'medium' / 'short' | Monday, June 15, 2009 · June 15, 2009 · Jun 15, 2009 · 6/15/09 |
| timeStyle | 'full' / 'long' / 'medium' / 'short' | 1:45:30 PM PDT (long) · 1:45:30 PM · 1:45 PM |
| calendar | 'gregory' / 'iso8601' / 'islamic' / 'hebrew' / … | the calendar system |
| numberingSystem | 'latn' / 'arab' / … | the digit system |
The two machine-format answers.
date.toISOString() is always 2009-06-15T20:45:30.617Z
— ISO 8601, always in UTC (that trailing Z). And to build a bare
YYYY-MM-DD for an
<input type="date">, assemble it from the local getters —
not toISOString().slice(0, 10), which
reads the UTC date and can land on a different calendar day (see the recipes and gotchas). For
Intl.DateTimeFormat, reuse one formatter instance across many values — constructing it
is the expensive part. Full grammar:
Intl.DateTimeFormat options.
Library tokens — the same output, four dialects
| Unit | date-fns | Luxon | Day.js / Moment |
|---|---|---|---|
| Year, 4-digit | yyyy | yyyy | YYYY |
| Year, 2-digit | yy | yy | YY |
| Month, 2-digit | MM | MM | MM |
| Month, full name | MMMM | MMMM | MMMM |
| Day of month, 2-digit | dd | dd | DD |
| Day of week, full | EEEE | EEEE | dddd |
| Hour, 24-hour, 2-digit | HH | HH | HH |
| Hour, 12-hour, 2-digit | hh | hh | hh |
| Minute, 2-digit | mm | mm | mm |
| Second, 2-digit | ss | ss | ss |
| AM / PM | a | a | A |
Nothing matches that search. .
The YYYY-vs-yyyy trap.
So 2009-06-15 13:45 is
"yyyy-MM-dd HH:mm" in date-fns and Luxon but
"YYYY-MM-DD HH:mm" in Day.js and Moment. And it’s not just cosmetic: in
date-fns, uppercase YYYY is the
week-numbering year and D/DD is the day of the year —
different fields from the calendar year and day-of-month, and the opposite convention from
Day.js/Moment (where YYYY/DD are the ordinary ones). Reach for
YYYY in date-fns by habit and you’ll get the wrong year on the days around New
Year’s (date-fns even warns you in the console). Luxon
sidesteps the collision: its week-numbering year is kkkk and its day-of-year is
o, while D/DD are localized-date presets
(6/15/2009 / Jun 15, 2009)
— YYYY isn’t a Luxon token at all. Temporal has no token language —
format it with toLocaleString / Intl.DateTimeFormat, or read its ISO string
from .toString(). Token grammars:
date-fns,
Luxon,
Day.js.
JSON & API timestamps
That 2009-06-15T20:45:30.617Z shape in JSON payloads is ISO 8601
in UTC (the trailing Z, “Zulu,” is the +00:00 offset). A
Date writes it automatically on JSON.stringify — but reading is the trap.
Write — to JSON
// Date.toJSON() runs automatically: JSON.stringify({ at: d }); // {"at":"2009-06-15T20:45:30.617Z"} d.toISOString(); // same string, always UTC
toISOString() / toJSON() throw on an Invalid Date.
Read — back to an instant
// JSON.parse does NOT revive dates — // you get a string back. Revive it: JSON.parse(text, (k, v) => typeof v === "string" && /^\d{4}-\d\d-\d\dT/.test(v) ? new Date(v) : v); // or: Temporal.Instant.from(str)
Or read into Temporal.Instant.from(str) — no Kind to guess.
JSON.parsereturns the timestamp as a string — it never revives aDate. Use a reviver.toISOString()is always UTC (Z); there is no built-in “with offset” ISO output onDate.Date.now()andgetTime()are milliseconds — multiply Unix seconds by 1000.Temporalserializes viatoString()/toJSON()(RFC 9557 extended ISO) and reads back with.from().
Reference:
Date.prototype.toJSON
and JSON.parse reviver.
Showing 0 of 0 answers
No answers match that topic. .
1 · The types & which to use
Why is the built-in Date considered broken?
Date is the canonical example of a bad date API, and its problems are structural. It is mutable; its month argument is 0-indexed (so new Date(2009, 0, 1) is January) while day-of-month is 1-indexed; two Dates are compared by reference, so a === b is almost always false; it reports epoch time in milliseconds (the off-by-1000 trap against Unix-seconds APIs); it parses non-ISO strings in implementation-defined ways; and it models only “local” and “UTC” with no real time-zone support. Each of these has its own section below.
new Date(2009, 0, 1); // Jan 1, 2009 — month 0 is January (!) new Date() === new Date(); // false — reference equality Date.now(); // milliseconds, not seconds
For nine years the workaround was a library (Moment, then Luxon / date-fns / Day.js). The real fix is Temporal, which is immutable, time-zone-aware, and unambiguous by construction.
Source: MDN — Date.
What are the Temporal types, and which should I use?
Temporal splits the one overloaded Date into purpose-named, immutable types. Temporal.Instant is an exact moment on the timeline (like an epoch reading). Temporal.ZonedDateTime is an instant plus an IANA time zone — the fullest type, and the right default for “a moment, somewhere.” Temporal.PlainDate, PlainTime, and PlainDateTime are wall values with no zone (a birthday, an opening time). PlainYearMonth and PlainMonthDay cover partial dates; Temporal.Duration is an amount of time; and Temporal.Now is the clock.
Temporal.Now.instant(); // an exact moment (Instant) Temporal.Now.zonedDateTimeISO(); // moment + your IANA zone Temporal.PlainDate.from("2009-06-15"); // a wall date, no zone Temporal.Instant.from("2009-06-15T20:45:30.617Z");
Rule of thumb: an absolute instant → Instant or ZonedDateTime; a wall date/time with no zone → PlainDate/PlainTime; an amount → Duration. Temporal reached Stage 4 in March 2026 and is slated for ECMAScript 2027; it ships in current engines, with a polyfill for Safari (as of July 2026).
Source: MDN — Temporal / TC39 Temporal docs.
2 · Getting “now”
How do I get the current date/time?
Date.now() is the fast path to the current epoch in milliseconds (a number, no object allocated). new Date() gives a Date object for the same instant. For a Temporal value, Temporal.Now.instant() or Temporal.Now.zonedDateTimeISO(). And for elapsed time, none of these — use performance.now(), which is monotonic (see the precision section).
Date.now(); // 1245098730617 (ms since epoch) new Date(); // a Date object at "now" Temporal.Now.zonedDateTimeISO(); // now, in your IANA zone performance.now(); // monotonic — for measuring, not clock time
Source: Date.now / Temporal.Now.
3 · Formatting
How do I format a date to a string?
There is no native format string (see the reference above). For human-readable output, use toLocaleString / toLocaleDateString / toLocaleTimeString with an Intl.DateTimeFormat options object; for machine output, toISOString(). Build one Intl.DateTimeFormat and reuse it — constructing the formatter is the expensive part. The legacy toString / toDateString / toUTCString exist but are fixed-format and rarely what you want.
const fmt = new Intl.DateTimeFormat("en-US", { dateStyle: "long", timeStyle: "short", timeZone: "America/Los_Angeles" }); fmt.format(d); // "June 15, 2009 at 1:45 PM" d.toISOString(); // "2009-06-15T20:45:30.617Z" — machine output
No token syntax natively — reach for Intl, a library, or Temporal.toLocaleString.
Source: MDN — Intl.DateTimeFormat.
4 · Parsing
How do I parse a date string — and why is new Date('2025-03-15') a day off?
ISO 8601 is the only format you can rely on across engines; anything else ("June 15, 2009", "06/15/2009") is implementation-defined and may differ between browsers. The headline gotcha: a bare date "2025-03-15" parses as UTC midnight, but the same value with a time "2025-03-15T00:00" parses as local midnight — so in a US zone the first can display as March 14. Date.parse is the same parser as new Date(string), returning ms (or NaN). Temporal’s from methods are strict and unambiguous.
new Date("2025-03-15"); // UTC midnight → Mar 14 evening in the US new Date("2025-03-15T00:00"); // LOCAL midnight → Mar 15 Temporal.PlainDate.from("2025-03-15"); // a date, no zone, no surprise
To parse a non-ISO layout, use a library’s explicit parser rather than trusting new Date(string).
Source: MDN — Date.parse (“date-only strings are treated as UTC”).
5 · Time zones
How do I work with time zones?
Native Date knows only two zones: the runtime’s local zone and UTC. You can format in any IANA zone with Intl.DateTimeFormat’s timeZone option, and read the local zone from Intl.DateTimeFormat().resolvedOptions().timeZone — but you can’t do zone math on a Date. That’s what Temporal.ZonedDateTime is for: it carries an IANA zone and does DST-aware arithmetic, with an explicit disambiguation option for the ambiguous (fall-back) and skipped (spring-forward) local times. JavaScript is IANA-only — there’s no Windows-vs-IANA zone-id tripwire (the one place JS is simpler than C#).
new Intl.DateTimeFormat("en-US", { timeZone: "Asia/Tokyo", dateStyle: "short", timeStyle: "short" }).format(d); new Intl.DateTimeFormat().resolvedOptions().timeZone; // "America/New_York" Temporal.Now.instant().toZonedDateTimeISO("Asia/Tokyo"); // zone math
Source: Intl timeZone / Temporal.ZonedDateTime.
6 · Converting to/from UTC
How do I read and build UTC values?
Every Date getter has a UTC twin: getHours() reads local, getUTCHours() reads UTC; likewise getUTCFullYear, getUTCMonth, getUTCDate, and so on. To build from UTC parts, Date.UTC(...) returns the epoch ms (whereas the new Date(y, m, d, ...) constructor interprets its parts as local). toISOString() always emits UTC. With Temporal, convert between an absolute Instant and a zoned view with .toZonedDateTimeISO(zone) / .toInstant().
d.getUTCHours(); // 20 (UTC) vs d.getHours() → local Date.UTC(2009, 5, 15, 20, 45, 30); // epoch ms from UTC parts (month 5 = June) d.toISOString(); // "2009-06-15T20:45:30.617Z" — always UTC
Source: Date.UTC / toISOString.
7 · Arithmetic
How do I add or subtract time?
With native Date there is no addDays — you mutate in place with setDate(getDate() + 1) (which conveniently rolls over month boundaries) or do epoch-ms math. Both are error-prone: the mutation changes the original, and a “day” of 86_400_000 ms isn’t always 24 hours across a DST boundary. Temporal is immutable and calendar-aware: .add() / .subtract() take a Duration and return a new value, respecting DST and month lengths.
const d2 = new Date(d); // copy first — setDate mutates! d2.setDate(d2.getDate() + 1); // +1 day (rolls over months) Temporal.PlainDate.from("2009-06-15").add({ days: 1 }); // → 2009-06-16, immutable, calendar-aware
Source: Date.setDate / Temporal.Duration.
8 · Differences between two dates
How do I get the difference between two dates?
Subtract them — the - operator coerces each Date to its epoch milliseconds, so the result is a number of ms. Divide by 86_400_000 for days, 3_600_000 for hours, and so on. There is no native “3 months, 2 days” breakdown — months and years aren’t fixed lengths. Temporal gives calendar-aware differences with .since() / .until(), which return a Duration.
const ms = date2 - date1; // coerced to epoch ms const days = ms / 86_400_000; // e.g. 30 Temporal.PlainDate.from("2009-07-15") .since("2009-06-15", { largestUnit: "months" }); // P1M
Source: Date.getTime / Temporal — since.
9 · Comparison & equality
Why does === on two dates return false?
The headline JavaScript date gotcha: a Date is an object, so === and == compare references, not instants. Two Dates built from the same moment are different objects, so a === b is false. Compare the underlying epoch numbers instead — a.getTime() === b.getTime(), or the unary-plus shorthand +a === +b. The relational operators (<, >, <=) do work, because they coerce each side to a number. With Temporal, use .equals() and the static compare().
const a = new Date("2009-06-15T20:45:30.617Z"); const b = new Date("2009-06-15T20:45:30.617Z"); a === b; // false — different objects a.getTime() === b.getTime(); // true +a === +b; // true — unary-plus shorthand a < b; // works — operators coerce to number
Source: Date.getTime / Temporal — equals.
10 · Min / max & “no value”
What’s the range, and how do I detect an invalid date?
A Date can represent ±8,640,000,000,000,000 ms from the epoch — about ±273,790 years, out to the year ±275,760. Past that you get an Invalid Date, which is also what any unparseable input produces (new Date("nope")). An Invalid Date’s getTime() is NaN, so test with Number.isNaN(d.getTime()) — not d === "Invalid Date" or d === null. There is no null-date type; use null/undefined for “no value.”
const d = new Date("nope"); d.getTime(); // NaN Number.isNaN(d.getTime()); // true → it's invalid new Date(8_640_000_000_000_000).toISOString(); // "+275760-09-13T00:00:00.000Z" — the max
11 · Unix / epoch time
How do I convert to/from a Unix timestamp? Is there a 2038 problem?
JavaScript is epoch-native, but in milliseconds: Date.now(), getTime(), and valueOf() all return ms since 1970-01-01 UTC, and new Date(ms) reads one back. The number-one interop bug is mixing this with Unix seconds APIs — multiply Unix seconds by 1000 going in, divide (and floor) going out. There is no year-2038 problem: the epoch is a 64-bit float of milliseconds, not a 32-bit signed integer of seconds. Temporal reads epoch values with fromEpochMilliseconds / fromEpochNanoseconds.
Date.now(); // 1245098730617 — MILLISECONDS new Date(unixSeconds * 1000); // seconds → Date (don't forget × 1000) Math.floor(Date.now() / 1000); // Date → Unix seconds Temporal.Instant.fromEpochMilliseconds(1245098730617);
Source: Date.getTime / Temporal.Instant.fromEpochMilliseconds.
12 · Precision & measuring elapsed time
How do I time an operation? (Why not Date.now()?)
Use performance.now(), never Date.now(). The wall clock is not monotonic — an NTP sync or a manual clock change can move it backward mid-measurement, giving a negative or wildly wrong elapsed time. performance.now() is backed by a monotonic clock and is sub-millisecond, though browsers intentionally clamp and jitter its resolution (a Spectre mitigation), so it’s for measuring durations, not for a high-precision wall clock. Date resolution is whole milliseconds.
const t0 = performance.now(); doWork(); const elapsedMs = performance.now() - t0; // monotonic, sub-ms // Never: Date.now() - start (wall clock can jump)
Rule: Date / Temporal answer “what time is it?”; performance.now() answers “how long did that take?”
Source: MDN — performance.now().
13 · Mutability & the Date traps
Mutable, 0-indexed months, reference equality — the traps that explain a decade of bugs
Three design choices account for most JavaScript date bugs. Mutability: setMonth, setDate, setHours, … mutate the Date in place and return a timestamp number, not a new Date — so a value you passed to a function can be changed under you, and chaining doesn’t work as it looks. 0-indexed months: months run 0–11 (January is 0) while day-of-month runs 1–31 — the “why is my month off by one” bug. Reference equality: two Dates are never === (see comparison). Temporal is immutable, so none of these can happen.
const d = new Date(2009, 5, 15); // month 5 = JUNE, not May const ret = d.setMonth(0); // mutates d AND returns a number (ms) // ret is 1230966000000, NOT a Date; d itself is now January
Defensive habits: copy before mutating (new Date(d)), always +1 when reading/writing a human month, and never compare with ===. Or skip all of it and use Temporal.
Source: Date.setMonth / getMonth (0–11).
14 · Serialization
How do dates serialize (JSON, structuredClone)?
JSON.stringify calls Date.prototype.toJSON, which emits the ISO 8601 UTC string (the toISOString() shape). JSON.parse does not reverse this — it has no idea a string was a date, so you get a string back and must revive it with a reviver function (see the JSON section). structuredClone (and the structured-clone algorithm used by postMessage / IndexedDB) does preserve a real Date. Temporal values serialize through toString()/toJSON() as RFC 9557 extended ISO strings and read back with the matching .from().
JSON.stringify({ at: d }); // {"at":"2009-06-15T20:45:30.617Z"} typeof JSON.parse('{"at":"...Z"}').at; // "string" — NOT revived structuredClone(d) instanceof Date; // true — preserved
Source: Date.toJSON / structuredClone.
15 · Common recipes
Start of day, add days, YYYY-MM-DD for an <input>, days between, is-weekend, is-leap-year, next Monday?
The everyday one-liners. Note the <input type="date"> recipe assembles the string from the local getters — using toISOString().slice(0, 10) would emit the UTC date and can be a day off.
// Start of day (local, immutable copy): const sod = new Date(d); sod.setHours(0, 0, 0, 0); // Add days safely (copy, don't mutate the original): const plus7 = new Date(d); plus7.setDate(plus7.getDate() + 7); // YYYY-MM-DD for <input type="date"> — LOCAL, not toISOString(): const p = (n) => String(n).padStart(2, "0"); const ymd = `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`; // Days between (whole days): const days = Math.round((b - a) / 86_400_000); // Is weekend / is leap year: const weekend = [0, 6].includes(d.getDay()); // 0 = Sun, 6 = Sat const leap = (y) => y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0); // Next Monday (on or after tomorrow): const nm = new Date(d); nm.setDate(nm.getDate() + ((8 - nm.getDay()) % 7 || 7));
Temporal.PlainDate makes the date-only recipes cleaner — .add({days}), .dayOfWeek, .toString() is already YYYY-MM-DD, no time component to strip.
Source: Date.setHours / getDay.
16 · The libraries
Luxon vs date-fns vs Day.js vs Moment — which do I pick?
Four libraries are in common use; the choice is a set of tradeoffs, not a ranking.
Luxon — immutable, Intl/IANA-based, from a Moment maintainer. The natural pick when you want a modern, immutable object API with strong time-zone handling. date-fns — a functional, tree-shakeable toolkit of standalone helpers that operate on native Date; TypeScript-first, pay only for the functions you import. Day.js — ~2 KB with a nearly Moment-compatible API; the least-painful migration path off Moment. Moment — the original, now in maintenance mode: its own maintainers recommend against it for new projects (large, mutable, not tree-shakeable). Keep it for legacy code; don’t start new code on it.
// Luxon date-fns Day.js / Moment DateTime.now() format(new Date(), "yyyy-MM-dd") dayjs().format("YYYY-MM-DD")
Going forward, Temporal reduces the need for a library in many cases — immutable types, zones, and arithmetic are now built in. A library still earns its place for token-string formatting, relative-time strings, or the widest browser reach before Temporal is universal.
Sources: Luxon · date-fns · Day.js · Moment project status.
The foot-guns, in one place
- Months are 0-indexed (January is 0) but day-of-month is 1-indexed — the “month off by one” bug.
Dateis mutable;setMonth/setDatechange it in place and return a number, not a newDate.===on twoDates is reference equality — comparegetTime()(or+a === +b).Date.now()/getTime()are milliseconds, not seconds — ×1000 against Unix-seconds APIs.new Date("2025-03-15")is parsed as UTC, but"2025-03-15T00:00"as local.- Non-ISO strings in
new Date(str)/Date.parseare implementation-defined. toISOString().slice(0, 10)gives the UTC date — build a localYYYY-MM-DDfrom the getters.- An
Invalid Date’sgetTime()isNaN— test withNumber.isNaN(d.getTime()). JSON.parsedoes not revive dates — you get a string; use a reviver.- Moment is legacy; and
YYYY/DDdiffer between date-fns/Luxon and Day.js/Moment.
Every answer links its primary source inline — the
MDN documentation
for Date, Intl.DateTimeFormat, JSON, and performance.now(), the
Temporal reference
and the TC39 Temporal docs,
and the Luxon /
date-fns /
Day.js /
Moment docs for the library survey.
Temporal’s timeline placement cross-links the
JavaScript version reference. Last updated July 2026.
Mungomash LLC · More on JavaScript