Convert a Legacy Date to a Temporal.Instant and Back

To move between a legacy Date and Temporal, go through the epoch-millisecond bridge: date.toTemporalInstant() (or Temporal.Instant.fromEpochMilliseconds(date.getTime())) converts forward, and new Date(instant.epochMilliseconds) converts back. This page is part of Working with ZonedDateTime Objects.

Why this scenario is tricky

A JavaScript Date is, internally, a single number: milliseconds since the Unix epoch in UTC. A Temporal.Instant is the same idea at higher resolution — nanoseconds since the epoch. Because both are pure absolute-time values with no attached time zone, the conversion between them is lossless in both directions for the millisecond range, provided you bridge on the epoch number rather than on a formatted string.

The trap is that developers reach for the string path — new Date(instant.toString()) or Temporal.Instant.from(date.toISOString()) — which works but is slower, allocates, and reintroduces parsing edge cases the numeric path avoids entirely. The other trap is resolution: a Date only holds milliseconds, so converting a nanosecond-precise Instant back to a Date truncates sub-millisecond digits. That is expected and fine for most apps, but it means instant → Date → instant is not always an identity round-trip.

A subtle point: an Instant is timezone-free. If you need wall-clock fields (year, month, hour) you must attach a zone with instant.toZonedDateTimeISO(timeZone) first — see the patterns in Compare ZonedDateTime Across Different Timezones and the broader introduction in Getting Started with the Temporal API.

The diagram shows the epoch number as the shared bridge between the two types.

Date and Temporal.Instant interop via the epoch bridge Date.getTime gives epoch ms to build an Instant; instant.epochMilliseconds rebuilds a Date, truncating nanoseconds. Date epoch ms (UTC) epoch number the shared bridge Temporal.Instant epoch ns (UTC) getTime() back: new Date(instant.epochMilliseconds) — nanoseconds truncated Both are timezone-free absolute time

API reference

Direction Call Notes
Date → Instant date.toTemporalInstant() Polyfill adds this to Date.prototype; lossless (ms → ns)
Date → Instant Temporal.Instant.fromEpochMilliseconds(date.getTime()) Explicit, no prototype dependency
Instant → Date new Date(instant.epochMilliseconds) Truncates sub-ms precision
Instant → wall clock instant.toZonedDateTimeISO(timeZone) Required before reading local fields
Instant → ms / ns instant.epochMilliseconds / instant.epochNanoseconds ms is number, ns is bigint

Minimal working solution

The shortest correct round-trip uses the numeric epoch bridge in both directions.

import { Temporal } from '@js-temporal/polyfill';

const legacy = new Date('2026-06-19T12:00:00.250Z');

// Date → Instant: pass epoch milliseconds straight into Temporal
const instant = Temporal.Instant.fromEpochMilliseconds(legacy.getTime());
console.log(instant.toString()); // '2026-06-19T12:00:00.25Z'

// Instant → Date: epochMilliseconds is a plain number Date accepts
const roundTrip = new Date(instant.epochMilliseconds);
console.log(roundTrip.getTime() === legacy.getTime()); // true — lossless at ms

The polyfill helper: toTemporalInstant()

The TC39 design adds a toTemporalInstant() method to Date.prototype. The @js-temporal/polyfill installs it for you, so once the polyfill is imported you can call it directly — it is exactly equivalent to the fromEpochMilliseconds(getTime()) form but reads more naturally.

import { Temporal } from '@js-temporal/polyfill';

const legacy = new Date('2026-06-19T12:00:00Z');

// Prototype method installed by the polyfill — equivalent to the explicit form
const a = legacy.toTemporalInstant();
const b = Temporal.Instant.fromEpochMilliseconds(legacy.getTime());

console.log(a.equals(b)); // true — same epoch instant

If you support environments where the prototype patch might be stripped (aggressive tree-shaking, frozen prototypes), prefer the explicit fromEpochMilliseconds form so the conversion never depends on a mutated built-in.

Full production version

A robust interop layer validates the Date, converts forward, optionally projects into a zone for display, and converts back — never throwing on a valid value and never silently accepting an Invalid Date.

import { Temporal } from '@js-temporal/polyfill';

/** Convert a legacy Date to a Temporal.Instant, rejecting Invalid Date. */
export function dateToInstant(date: Date): Temporal.Instant {
  if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
    throw new TypeError('dateToInstant requires a valid Date');
  }
  // getTime() is epoch ms; fromEpochMilliseconds is the lossless numeric bridge
  return Temporal.Instant.fromEpochMilliseconds(date.getTime());
}

/** Convert a Temporal.Instant back to a Date (sub-millisecond precision truncates). */
export function instantToDate(instant: Temporal.Instant): Date {
  // epochMilliseconds is a Number; Date holds only ms, so ns digits are dropped
  return new Date(instant.epochMilliseconds);
}

/** Project a Date into a zone for wall-clock fields, then return a fresh Date. */
export function shiftToZone(date: Date, timeZone: string): Temporal.ZonedDateTime {
  return dateToInstant(date).toZonedDateTimeISO(timeZone); // attaches the IANA zone
}

const now = new Date('2026-06-19T12:00:00.250Z');
const inst = dateToInstant(now);
console.log(instantToDate(inst).toISOString());          // '2026-06-19T12:00:00.250Z'
console.log(shiftToZone(now, 'Asia/Tokyo').toString());
// '2026-06-19T21:00:00.25+09:00[Asia/Tokyo]' — same instant, Tokyo wall clock

Verification snippet

These assertions confirm both conversion forms agree, the millisecond round-trip is exact, and nanosecond precision truncates exactly as documented.

import { Temporal } from '@js-temporal/polyfill';

const date = new Date('2026-06-19T12:00:00.250Z');

// Both forward conversions produce the same Instant.
const viaMethod = date.toTemporalInstant();
const viaStatic = Temporal.Instant.fromEpochMilliseconds(date.getTime());
console.assert(viaMethod.equals(viaStatic), 'method and static form agree');

// Round-trip Date → Instant → Date is exact at millisecond resolution.
const back = new Date(viaStatic.epochMilliseconds);
console.assert(back.getTime() === date.getTime(), 'ms round-trip is lossless');

// Going Instant(ns) → Date drops sub-millisecond digits.
const nanoInstant = Temporal.Instant.fromEpochNanoseconds(1_000_000_500n); // 1000ms + 500ns
const lossy = new Date(nanoInstant.epochMilliseconds);
console.assert(lossy.getTime() === 1000, 'Date truncates the 500ns remainder');
console.log('All interop assertions passed');

Common pitfalls

Frequently Asked Questions

Is converting a Date to a Temporal.Instant lossless?

Yes, going forward. A Date stores epoch milliseconds, and fromEpochMilliseconds (or toTemporalInstant()) preserves that value exactly inside the higher-resolution Instant. The reverse direction can lose precision because a Date cannot hold the sub-millisecond nanoseconds an Instant may carry.

Where does Date.prototype.toTemporalInstant() come from?

It is part of the TC39 Temporal proposal and is installed on Date.prototype by @js-temporal/polyfill. It is equivalent to Temporal.Instant.fromEpochMilliseconds(date.getTime()); prefer the explicit static form if you cannot rely on the prototype being patched.

How do I get local date and time fields after converting a Date?

An Instant is timezone-free, so call instant.toZonedDateTimeISO(timeZone) to attach an IANA zone, then read .year, .month, .hour, and related properties on the resulting ZonedDateTime.