JavaScript
Date pitfalls, the Temporal proposal, and Intl for formatting.
JavaScriptβs Date object represents a single instant stored internally as milliseconds since the Unix epoch (1970-01-01T00:00:00 UTC). It has no concept of a stored time zone β it is always UTC under the hood. Methods fall into two families: getUTC*() reads UTC fields, while get*() (e.g. getHours()) reads fields in the runtimeβs local zone. This split is the source of most JS date confusion.
Parsing with new Date(string) is treacherous. A date-only string like new Date("2026-06-05") is parsed as UTC midnight per the ECMAScript spec, so in any timezone west of UTC it will display as June 4th. Date-time strings ("2026-06-05T14:00:00") with no offset are parsed as local time. Non-ISO formats have always been implementation-defined and have diverged between browsers β avoid them entirely.
There is no first-class IANA-zone-aware type in legacy JS. For display and formatting in a given time zone, use Intl.DateTimeFormat with a timeZone option, or the toLocaleString shorthand. For serious time-zone arithmetic you currently need a library such as Luxon or date-fns-tz.
The Temporal proposal
Temporal is the TC39 proposal that redesigns date/time from the ground up. It introduces distinct types β Temporal.Instant (an absolute point in time), Temporal.ZonedDateTime (instant + IANA zone), Temporal.PlainDate, Temporal.PlainDateTime, etc. β so the naive/aware confusion is eliminated at the type level.
Pitfall: new Date("2026-06-05") (date-only ISO string) is parsed as UTC midnight, not local midnight. Calling .getDate() on it in a UTCβ5 timezone returns 4, not 5. Always append a time and explicit offset, or use a Temporal.PlainDate once Temporal is available, to avoid this off-by-one-day error.
Pitfall: Temporal is a Stage 3 TC39 proposal but not yet shipped in every engine β browser support is still rolling out and varies. Check current support and use a polyfill (or an engine version check) in production rather than assuming Temporal is available.
Go deeper: Temporal types, Intl, and safe parsing patterns
Temporal types at a glance:
| Type | What it represents |
|---|---|
Temporal.Instant |
An absolute point in time (like a Unix timestamp) |
Temporal.ZonedDateTime |
Instant + IANA zone β knows its civil reading |
Temporal.PlainDate |
Calendar date, no time, no zone |
Temporal.PlainTime |
Time of day, no date, no zone |
Temporal.PlainDateTime |
Date + time, no zone (civil/local) |
Temporal.Duration |
A length of time |
Intl for formatting today:
const formatter = new Intl.DateTimeFormat("en-GB", {
timeZone: "Europe/Paris",
dateStyle: "long",
timeStyle: "short",
});
formatter.format(new Date()); // e.g. "5 June 2026 at 16:00"
Safe parsing with legacy Date:
Always include an explicit offset to avoid ambiguity:
// Unambiguous: always UTC
new Date("2026-06-05T14:00:00Z");
// Unambiguous: explicit offset
new Date("2026-06-05T14:00:00+02:00");
// Ambiguous β avoid: no offset on a datetime string means local time
new Date("2026-06-05T14:00:00");
Because server-side JavaScript (Node.js) runs in a predictable environment, precomputing formatted times server-side and sending them as strings is often safer than relying on client-side Date parsing, especially for initial page renders.
See also Instant vs civil time and Parsing user input.
Canonical values
The same valid ISO 8601 / RFC 3339 values, parsed by the legacy Date and by Temporal. See also parsing user input for the mirror problem β sloppy input across engines.
Generated at build time Β· 22.22.3 Β· @js-temporal/polyfill 0.5.1 Β· rendered in America/Los_Angeles
- β Handled losslessly β or correctly rejected a value this type should not accept.
- β οΈ Accepted, but lossy or surprising: precision truncated, offset discarded, or a silent assumption made.
- β Wrong or unsafe for the input β silently corrupted or invented information (a guessed or shifted value), or a hard rejection of a value that is valid under a standard the tool claims to support.
Date only
2026-06-12
Calendar date β no time, no zone
ISO 8601 not RFC 3339
-
Date
β
Jun 11, 2026, 5:00:00 PM PDT -
Temporal
β
2026-06-12
Go deeper: every method
new Date(β¦).toISOString() |
2026-06-12T00:00:00.000Z |
β οΈ |
|---|---|---|
| Coerced to an absolute instant (UTC midnight) the string never specified. | ||
getTime() |
1781222400000 |
β οΈ |
| An epoch value invented from a zone-less calendar date. | ||
render @ America/Los_Angeles |
Jun 11, 2026, 5:00:00 PM PDT |
β |
| Parsed as UTC midnight, so west of UTC it renders as the previous calendar day (Jun 11). | ||
Temporal.Instant.from(β¦) |
RangeError: Temporal.Instant requires a time zone offset | β |
|---|---|---|
| Refuses to invent an instant from a zone-less date β exactly the right call. | ||
Temporal.PlainDateTime.from(β¦) |
2026-06-12T00:00:00 |
β |
| Parses to a zone-less date-time at midnight, no instant assumed. | ||
Temporal.PlainDate.from(β¦) |
2026-06-12 |
β |
| Parses to a plain calendar date β the type that actually matches the input. | ||
Gotcha: per the ECMAScript spec a date-only string is parsed as midnight UTC, not local midnight. In any zone west of UTC, rendering it back as a local date shows the previous day. (Note the asymmetry with local date-time, which is parsed as local time.)
Local date-time
2026-06-12T12:34:56.123456789
Date and time, no zone
ISO 8601 not RFC 3339
-
Date
β οΈ
Jun 12, 2026, 5:34:56 AM PDT -
Temporal
β
2026-06-12T12:34:56.123456789
Go deeper: every method
new Date(β¦).toISOString() |
2026-06-12T12:34:56.123Z |
β οΈ |
|---|---|---|
| Assumes browser-local time and truncates the nanoseconds to .123 ms. | ||
getTime() |
1781267696123 |
β οΈ |
| Epoch derived from a local-time assumption the string never made. | ||
render @ America/Los_Angeles |
Jun 12, 2026, 5:34:56 AM PDT |
β οΈ |
| Interprets the zone-less time as browser-local β the rendered instant varies per reader. | ||
Temporal.Instant.from(β¦) |
RangeError: Temporal.Instant requires a time zone offset | β |
|---|---|---|
| Refuses to fabricate an instant from a zone-less date-time. | ||
Temporal.PlainDateTime.from(β¦) |
2026-06-12T12:34:56.123456789 |
β |
| Preserves the full nanosecond-precision wall-clock time with no zone assumption. | ||
Temporal.PlainDate.from(β¦) |
2026-06-12 |
β |
| Extracts just the calendar date, dropping the time component as requested. | ||
Note: with no zone, Date assumes browser-local time, so the absolute instant differs per reader. And Date stores only milliseconds β .123456789 truncates to .123; Temporal keeps nanoseconds.
Excess precision
2026-06-12T12:34:56.12345678987Z
11 fractional digits (beyond nanosecond), UTC
ISO 8601 RFC 3339
-
Date
β οΈ
2026-06-12T12:34:56.123Z -
Temporal
β
RangeError: invalid RFC 9557 string: 2026-06-12T12:34:56.12345678987Z
Go deeper: every method
new Date(β¦).toISOString() |
2026-06-12T12:34:56.123Z |
β οΈ |
|---|---|---|
| Silently truncates the 11-digit fraction to milliseconds (.123). | ||
getTime() |
1781267696123 |
β οΈ |
| Epoch is correct only to milliseconds; the sub-ms fraction is gone. | ||
render @ America/Los_Angeles |
Jun 12, 2026, 5:34:56 AM PDT |
β οΈ |
| Renders correctly but only to millisecond precision. | ||
Temporal.Instant.from(β¦) |
RangeError: invalid RFC 9557 string: 2026-06-12T12:34:56.12345678987Z | β |
|---|---|---|
| Rejects the value β 11 fractional digits exceed nanosecond precision rather than being silently dropped. | ||
Temporal.PlainDateTime.from(β¦) |
RangeError: invalid RFC 9557 string: 2026-06-12T12:34:56.12345678987Z | β |
| Rejects the over-precise fraction instead of truncating it. | ||
Temporal.PlainDate.from(β¦) |
RangeError: invalid RFC 9557 string: 2026-06-12T12:34:56.12345678987Z | β |
| Rejects the over-precise fraction instead of truncating it. | ||
Gotcha: 11 fractional digits exceed nanosecond resolution. Date silently truncates to milliseconds and Python to microseconds, so both accept a value they can't faithfully hold; Temporal refuses it outright.
With offset
2026-06-12T12:34:56.123456789+05:30
Date-time with explicit +05:30 offset
ISO 8601 RFC 3339
-
Date
β οΈ
2026-06-12T07:04:56.123Z -
Temporal
β
2026-06-12T07:04:56.123456789Z
Go deeper: every method
new Date(β¦).toISOString() |
2026-06-12T07:04:56.123Z |
β οΈ |
|---|---|---|
| Right instant, but the +05:30 offset is discarded and nanoseconds truncated to ms. | ||
getTime() |
1781247896123 |
β οΈ |
| Correct epoch, but the original offset and sub-ms fraction are lost. | ||
render @ America/Los_Angeles |
Jun 12, 2026, 12:04:56 AM PDT |
β οΈ |
| Localizes the instant correctly, but the source +05:30 offset is gone. | ||
Temporal.Instant.from(β¦) |
2026-06-12T07:04:56.123456789Z |
β |
|---|---|---|
| Converts to a UTC instant losslessly, preserving nanosecond precision. | ||
Temporal.PlainDateTime.from(β¦) |
2026-06-12T12:34:56.123456789 |
β οΈ |
| Keeps the wall-clock fields but silently drops the +05:30 offset. | ||
Temporal.PlainDate.from(β¦) |
2026-06-12 |
β οΈ |
| Extracts the date but discards both the time and the offset. | ||
Note: the +05:30 offset fixes the absolute instant, but it is not a time zone. Date keeps only the instant (offset gone); Temporal's PlainDateTime keeps the wall-clock fields and drops the offset, while Instant keeps the instant β pick the type that matches what you need.
UTC (Z)
2026-06-12T12:34:56.123456789Z
Date-time anchored to UTC via Z
ISO 8601 RFC 3339
-
Date
β οΈ
2026-06-12T12:34:56.123Z -
Temporal
β
2026-06-12T12:34:56.123456789Z
Go deeper: every method
new Date(β¦).toISOString() |
2026-06-12T12:34:56.123Z |
β οΈ |
|---|---|---|
| Correct instant, but the nanosecond fraction is truncated to milliseconds (.123). | ||
getTime() |
1781267696123 |
β οΈ |
| Epoch is correct but millisecond-only; nanoseconds are lost. | ||
render @ America/Los_Angeles |
Jun 12, 2026, 5:34:56 AM PDT |
β οΈ |
| Renders correctly but only to millisecond precision. | ||
Temporal.Instant.from(β¦) |
2026-06-12T12:34:56.123456789Z |
β |
|---|---|---|
| Exact instant preserved to nanoseconds. | ||
Temporal.PlainDateTime.from(β¦) |
RangeError: Z designator not supported for PlainDateTime | β |
| Rejects the Z designator β a plain date-time must not carry a UTC anchor. | ||
Temporal.PlainDate.from(β¦) |
RangeError: Z designator not supported for PlainDate | β |
| Rejects the Z designator β a plain date must not carry a UTC anchor. | ||
Note: a clean UTC instant β the easy case. Both Date and Python still truncate the nanosecond fraction (to ms and Β΅s respectively); only Temporal preserves all nine digits.
Space separator
2026-06-12 12:34:56Z
Space instead of T, UTC
not ISO 8601 RFC 3339
-
Date
β
2026-06-12T12:34:56.000Z -
Temporal
β
2026-06-12T12:34:56Z
Go deeper: every method
new Date(β¦).toISOString() |
2026-06-12T12:34:56.000Z |
β |
|---|---|---|
| Accepts the RFC 3339 space-separator variant and parses the instant correctly. | ||
getTime() |
1781267696000 |
β |
| Correct epoch for the UTC instant. | ||
render @ America/Los_Angeles |
Jun 12, 2026, 5:34:56 AM PDT |
β |
| Localizes the parsed UTC instant correctly. | ||
Temporal.Instant.from(β¦) |
2026-06-12T12:34:56Z |
β |
|---|---|---|
| Parses the space-separator RFC 3339 string to the correct instant. | ||
Temporal.PlainDateTime.from(β¦) |
RangeError: Z designator not supported for PlainDateTime | β |
| Rejects the Z designator β appropriate for a zone-less type. | ||
Temporal.PlainDate.from(β¦) |
RangeError: Z designator not supported for PlainDate | β |
| Rejects the Z designator β appropriate for a zone-less type. | ||
Note: the space separator is legal in RFC 3339 but not in ISO 8601, which requires a T. Every engine here accepts it anyway β a reminder that the two standards disagree at the edges.
Comma fraction
2026-06-12T12:34:56,123Z
Comma decimal sign, UTC
ISO 8601 not RFC 3339
-
Date
β
Invalid Date -
Temporal
β
2026-06-12T12:34:56.123Z
Go deeper: every method
new Date(β¦).toISOString() |
Invalid Date | β |
|---|---|---|
| Rejects the ISO 8601 comma decimal sign β a valid form Date cannot parse. | ||
getTime() |
NaN | β |
| Returns NaN on the comma-fraction form. | ||
render @ America/Los_Angeles |
Invalid Date | β |
| Invalid Date β cannot render the comma-fraction form. | ||
Temporal.Instant.from(β¦) |
2026-06-12T12:34:56.123Z |
β |
|---|---|---|
| Accepts the ISO 8601 comma fraction and parses the instant correctly. | ||
Temporal.PlainDateTime.from(β¦) |
RangeError: Z designator not supported for PlainDateTime | β |
| Rejects the Z designator β appropriate for a zone-less type. | ||
Temporal.PlainDate.from(β¦) |
RangeError: Z designator not supported for PlainDate | β |
| Rejects the Z designator β appropriate for a zone-less type. | ||
Gotcha: ISO 8601 permits a comma as the decimal sign (56,123); RFC 3339 requires a dot. Date rejects the comma form outright, while Temporal and Python stdlib accept it β the boundary between the two standards in one character.
Unknown offset
2026-06-12T12:34:56-00:00
Negative-zero offset
not ISO 8601 RFC 3339
-
Date
β οΈ
2026-06-12T12:34:56.000Z -
Temporal
β οΈ
2026-06-12T12:34:56Z
Go deeper: every method
new Date(β¦).toISOString() |
2026-06-12T12:34:56.000Z |
β οΈ |
|---|---|---|
| -00:00 (unknown offset per RFC 3339) is silently flattened to UTC +00:00. | ||
getTime() |
1781267696000 |
β οΈ |
| Epoch treats the unknown offset as plain UTC. | ||
render @ America/Los_Angeles |
Jun 12, 2026, 5:34:56 AM PDT |
β οΈ |
| Renders as UTC; the 'offset unknown' signal of -00:00 is lost. | ||
Temporal.Instant.from(β¦) |
2026-06-12T12:34:56Z |
β οΈ |
|---|---|---|
| Accepts -00:00 and normalizes to UTC, dropping the RFC 3339 'unknown offset' nuance. | ||
Temporal.PlainDateTime.from(β¦) |
2026-06-12T12:34:56 |
β οΈ |
| Keeps the wall-clock fields but discards the -00:00 offset entirely. | ||
Temporal.PlainDate.from(β¦) |
2026-06-12 |
β οΈ |
| Extracts the date and discards the offset. | ||
Gotcha: RFC 3339 uses -00:00 to mean “UTC, but the real local offset is unknown.” Every engine here collapses it to plain UTC +00:00, silently discarding that “unknown” nuance.
Lowercase t/z
2026-06-12t12:34:56z
Lowercase date-time designators
not ISO 8601 RFC 3339
-
Date
β
2026-06-12T12:34:56.000Z -
Temporal
β
2026-06-12T12:34:56Z
Go deeper: every method
new Date(β¦).toISOString() |
2026-06-12T12:34:56.000Z |
β |
|---|---|---|
| Accepts the lowercase t/z designators, which RFC 3339 declares case-insensitive. | ||
getTime() |
1781267696000 |
β |
| Correct epoch for the lowercase-designator instant. | ||
render @ America/Los_Angeles |
Jun 12, 2026, 5:34:56 AM PDT |
β |
| Localizes the parsed instant correctly. | ||
Temporal.Instant.from(β¦) |
2026-06-12T12:34:56Z |
β |
|---|---|---|
| Accepts the case-insensitive lowercase z and parses the instant correctly. | ||
Temporal.PlainDateTime.from(β¦) |
RangeError: Z designator not supported for PlainDateTime | β |
| Rejects the (lowercase) Z designator β appropriate for a zone-less type. | ||
Temporal.PlainDate.from(β¦) |
RangeError: Z designator not supported for PlainDate | β |
| Rejects the (lowercase) Z designator β appropriate for a zone-less type. | ||
Gotcha: RFC 3339 says the t and z designators are case-insensitive. Date and Temporal accept the lowercase form; Python's fromisoformat rejects it β a real interop hazard for case-careless producers.
Basic format
20260612T123456Z
No separators (ISO basic), UTC
ISO 8601 not RFC 3339
-
Date
β
Invalid Date -
Temporal
β
2026-06-12T12:34:56Z
Go deeper: every method
new Date(β¦).toISOString() |
Invalid Date | β |
|---|---|---|
| Rejects ISO 8601 basic (no-separator) format β returns Invalid Date. | ||
getTime() |
NaN | β |
| Returns NaN on the basic-format string. | ||
render @ America/Los_Angeles |
Invalid Date | β |
| Invalid Date β cannot render the basic-format string. | ||
Temporal.Instant.from(β¦) |
2026-06-12T12:34:56Z |
β |
|---|---|---|
| Accepts ISO basic format and parses the instant correctly. | ||
Temporal.PlainDateTime.from(β¦) |
RangeError: Z designator not supported for PlainDateTime | β |
| Rejects the Z designator β appropriate for a zone-less type. | ||
Temporal.PlainDate.from(β¦) |
RangeError: Z designator not supported for PlainDate | β |
| Rejects the Z designator β appropriate for a zone-less type. | ||
Gotcha: ISO 8601 “basic” format drops the separators (20260612T123456Z). Date returns Invalid Date and Python's date.fromisoformat rejects it, while datetime.fromisoformat and Temporal's Instant accept it β support is patchy.
Week date
2026-W24-3
ISO week-numbering date
ISO 8601 not RFC 3339
-
Date
β
Invalid Date -
Temporal
β
RangeError: invalid RFC 9557 string: 2026-W24-3
Go deeper: every method
new Date(β¦).toISOString() |
Invalid Date | β |
|---|---|---|
| Cannot parse ISO week-date notation β returns Invalid Date. | ||
getTime() |
NaN | β |
| Returns NaN on the week-date string. | ||
render @ America/Los_Angeles |
Invalid Date | β |
| Invalid Date β cannot render the week-date string. | ||
Temporal.Instant.from(β¦) |
RangeError: invalid RFC 9557 string: 2026-W24-3 | β |
|---|---|---|
| Rejects week-date notation, which RFC 9557 does not include. | ||
Temporal.PlainDateTime.from(β¦) |
RangeError: invalid RFC 9557 string: 2026-W24-3 | β |
| Rejects week-date notation, unsupported by RFC 9557. | ||
Temporal.PlainDate.from(β¦) |
RangeError: invalid RFC 9557 string: 2026-W24-3 | β |
| Rejects week-date notation, unsupported by RFC 9557. | ||
Gotcha: the ISO week-numbering date 2026-W24-3 is valid ISO 8601 but unsupported by Date and Temporal (it isn't in RFC 9557). Python does parse it, to the calendar date 2026-06-10 β correct, but easy to overlook.
US slash + 12h
5/12/23 5pm
Human-typed US-style date and time
not ISO 8601 not RFC 3339 human-friendly
-
Date
β
Invalid Date -
Temporal
β
RangeError: invalid RFC 9557 string: 5/12/23 5pm
Go deeper: every method
new Date(β¦).toISOString() |
Invalid Date | β |
|---|---|---|
| Refuses the ambiguous human-typed input β returns Invalid Date. | ||
getTime() |
NaN | β |
| Returns NaN rather than guessing at the ambiguous input. | ||
render @ America/Los_Angeles |
Invalid Date | β |
| Invalid Date β refuses to render the ambiguous input. | ||
Temporal.Instant.from(β¦) |
RangeError: invalid RFC 9557 string: 5/12/23 5pm | β |
|---|---|---|
| Rejects the non-ISO human-typed input. | ||
Temporal.PlainDateTime.from(β¦) |
RangeError: invalid RFC 9557 string: 5/12/23 5pm | β |
| Rejects the non-ISO human-typed input. | ||
Temporal.PlainDate.from(β¦) |
RangeError: invalid RFC 9557 string: 5/12/23 5pm | β |
| Rejects the non-ISO human-typed input. | ||
Gotcha: 5/12/23 5pm is human input, not a machine format β is that May 12 or December 5? Every engine here refuses it. Don't feed free-form text to a date constructor; parse it explicitly with a locale-aware library.
Spelled-out
June 5, 2026 5:00 PM
Human-typed, spelled-out month
not ISO 8601 not RFC 3339 human-friendly
-
Date
β
2026-06-05T17:00:00.000Z -
Temporal
β
RangeError: invalid RFC 9557 string: June 5, 2026 5:00 PM
Go deeper: every method
new Date(β¦).toISOString() |
2026-06-05T17:00:00.000Z |
β |
|---|---|---|
| Silently guesses at the spelled-out date via implementation-defined heuristics β returns a value where an error would be safer. | ||
getTime() |
1780678800000 |
β |
| Returns an epoch from a heuristic guess at free-form text. | ||
render @ America/Los_Angeles |
Jun 5, 2026, 10:00:00 AM PDT |
β |
| Renders a value the engine guessed at β behaviour varies across runtimes. | ||
Temporal.Instant.from(β¦) |
RangeError: invalid RFC 9557 string: June 5, 2026 5:00 PM | β |
|---|---|---|
| Rejects the non-ISO spelled-out input rather than guessing. | ||
Temporal.PlainDateTime.from(β¦) |
RangeError: invalid RFC 9557 string: June 5, 2026 5:00 PM | β |
| Rejects the non-ISO spelled-out input rather than guessing. | ||
Temporal.PlainDate.from(β¦) |
RangeError: invalid RFC 9557 string: June 5, 2026 5:00 PM | β |
| Rejects the non-ISO spelled-out input rather than guessing. | ||
Gotcha: JavaScript's Date happily guesses at June 5, 2026 5:00 PM β interpreting it in the runtime's local zone via implementation-defined heuristics. Temporal and Python reject it. A returned value here is more dangerous than an error.