In programming languages

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
Date
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
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
Date
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
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
Date
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
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
Date
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
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
Date
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
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
Date
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
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
Date
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
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
Date
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
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
Date
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
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
Date
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
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
Date
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
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
Date
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
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
Date
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
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.


← Back to all topics