JavaScript's Date object has carried the web for decades, but almost every experienced developer has a story about it: an off-by-one-day bug, a daylight-saving surprise, a month numbered from zero, or a date-only value that silently became a timestamp.
In July 2026, the TC39 Temporal proposal reached a Stage 4 draft. That is a major standardisation milestone: the API has completed the proposal process and is ready to become part of the ECMAScript standard.
But Stage 4 does not mean you can assume every current browser already has Temporal. MDN still marks the API as limited availability. The practical approach is to learn the model now, use feature detection, and add the official polyfill when your support policy requires it.
This guide explains the mental model, the most useful types, and a safe migration path from Date.
What you will learn
- how Temporal models dates, times, instants, zones, and durations
- which Temporal type fits each common use case
- how to migrate from Date without a risky rewrite
- how to handle current browser support safely
Why Date causes so many bugs
Date tries to represent several different concepts with one mutable object:
- an exact instant on the global timeline
- a calendar date such as a birthday
- a local wall-clock time
- a date and time in a named time zone
- a duration between two values
Those concepts are not interchangeable.
Consider the string 2026-08-10. It might mean a birthday, a billing date, or midnight in some time zone. Turning it into a Date can introduce a time zone that the original value never had.
Date also has historical API traps. Months are zero-indexed in the numeric constructor, parsing behaviour has edge cases, and methods can mutate an existing value.
Temporal solves the modelling problem by using separate immutable types.
The Temporal types you should know
You do not need to memorise the whole API. Start by choosing the type that matches the data.
Temporal.PlainDate
Use PlainDate for a calendar date with no time and no time zone.
Good examples include:
- birthdays
- public holidays
- invoice due dates
- check-in dates
const launchDate = Temporal.PlainDate.from("2026-08-10");
const reviewDate = launchDate.add({ months: 1 });
console.log(launchDate.toString()); // 2026-08-10
console.log(reviewDate.toString()); // 2026-09-10
Temporal values are immutable. add() returns a new value; it does not change launchDate.
Temporal.PlainTime
Use PlainTime for a wall-clock time without a date or time zone.
const openingTime = Temporal.PlainTime.from("09:30");
console.log(openingTime.hour); // 9
This works for a recurring local concept such as "the store opens at 09:30." It does not identify one exact moment globally.
Temporal.Instant
Use Instant for an exact point on the timeline.
This is the closest Temporal equivalent to a Unix timestamp or a Date used as an event timestamp.
const deployedAt = Temporal.Instant.from("2026-08-10T06:30:00Z");
console.log(deployedAt.epochMilliseconds);
Instants are useful for logs, database timestamps, audit events, and API data that ends in Z or includes an offset.
Temporal.ZonedDateTime
Use ZonedDateTime when the named time zone matters.
const meeting = Temporal.ZonedDateTime.from(
"2026-08-10T10:00:00+05:30[Asia/Kolkata]"
);
console.log(meeting.timeZoneId); // Asia/Kolkata
console.log(meeting.toString());
The named zone is important because an offset such as +05:30 and a time zone such as Asia/Kolkata are different kinds of information. Time-zone rules can change, and many regions use daylight saving time.
Temporal.Duration
Use Duration for an amount of time.
const sprint = Temporal.Duration.from({ weeks: 2 });
console.log(sprint.toString()); // P2W
Be careful when converting calendar units into clock units. A day is not always exactly 24 hours in a zone with daylight-saving changes. Temporal makes you provide the context when that context is required.
A DST-safe example
Imagine a recurring meeting at 10:00 in New York. Adding one calendar day should keep the meeting at 10:00 local time, even if the UTC offset changes.
const beforeChange = Temporal.ZonedDateTime.from(
"2026-03-07T10:00:00-05:00[America/New_York]"
);
const nextDay = beforeChange.add({ days: 1 });
console.log(nextDay.hour); // 10
console.log(nextDay.offset); // may change with DST rules
This is exactly why a named time zone is more useful than manually adding 86,400,000 milliseconds.
A practical migration map
Do not replace every Date in one giant refactor. First classify what each value means.
Date-only form values
Old approach:
const selected = new Date(input.value);
Safer Temporal model:
const selected = Temporal.PlainDate.from(input.value);
An HTML date input returns a date-only string. PlainDate preserves that meaning without introducing midnight or a time zone.
API timestamps
If an API returns an exact timestamp:
const createdAt = Temporal.Instant.from(apiResponse.createdAt);
If your UI needs a local representation, convert it with an explicit zone:
const local = createdAt.toZonedDateTimeISO("Asia/Kolkata");
console.log(local.toLocaleString());
Current time
For a timestamp:
const now = Temporal.Now.instant();
For the current calendar date in a chosen zone:
const todayInIndia = Temporal.Now.plainDateISO("Asia/Kolkata");
These two calls answer different questions, which is the point.
Calendar arithmetic
Old code often adds milliseconds:
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
For a calendar date, write the intent directly:
const tomorrow = Temporal.Now.plainDateISO().add({ days: 1 });
Interoperating with existing Date code
Most projects will use Date and Temporal together during migration.
Convert a Date to an Instant:
const legacyDate = new Date();
const instant = Temporal.Instant.from(legacyDate.toISOString());
Convert an Instant back to Date:
const legacyAgain = new Date(instant.epochMilliseconds);
This boundary is useful when an existing library still expects Date.
Do not convert a PlainDate to Date unless you decide what time and time zone that calendar date should represent. That decision cannot be inferred safely.
Browser support and the polyfill
Before using the global Temporal object, check your runtime support.
if ("Temporal" in globalThis) {
// Native Temporal is available.
}
For production code that must work in browsers without native support, use the @js-temporal/polyfill package and follow its current installation documentation.
import { Temporal } from "@js-temporal/polyfill";
A polyfill has bundle-size and compatibility implications, so apply it deliberately. For a small isolated date task, an existing well-tested library may still be the right short-term choice. For new domain modelling, Temporal gives the platform a consistent long-term direction.
Common mistakes to avoid
Using PlainDateTime for a real event
PlainDateTime has a date and clock time but no time zone. "10 August at 10:00" is not one exact global moment until a zone is supplied.
Use ZonedDateTime or Instant for events that must be coordinated across locations.
Storing only a formatted string
A string such as "10 Aug, 11:30 AM" is presentation, not durable data. Store a machine-readable Temporal string or the underlying structured value, then format it for the user.
Assuming an offset is a time zone
An offset tells you the relationship to UTC at one moment. A named time zone carries rules for future and historical changes.
Comparing objects with normal operators
Temporal objects intentionally avoid ambiguous coercion. Use methods such as Temporal.PlainDate.compare(), equals(), since(), or until().
Ignoring availability
Stage 4 is a standards milestone, not proof that all users have upgraded browsers. Test the environments in your support matrix and provide a polyfill or fallback when needed.
A simple decision checklist
Before creating a Temporal value, ask:
- Is this only a calendar date? Use PlainDate.
- Is this only a clock time? Use PlainTime.
- Is this one exact timestamp? Use Instant.
- Does a named time zone affect the meaning? Use ZonedDateTime.
- Is this an amount of time? Use Duration.
That small modelling decision prevents many bugs before arithmetic or formatting begins.
Conclusion: clearer intent is the real upgrade
Temporal is valuable not because it has more methods than Date, but because the type tells another developer what the value means.
A birthday is not a timestamp. A meeting in New York is not just an offset. One calendar day is not always 24 hours.
Temporal puts those distinctions into the API.
The safest adoption plan in 2026 is straightforward: classify existing date values, migrate one boundary at a time, use native support when available, and include the polyfill where your browser matrix needs it.
Sources
- https://tc39.es/proposal-temporal/
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal
- https://tc39.es/proposal-temporal/docs/
- https://www.npmjs.com/package/@js-temporal/polyfill
Tags: javascript, webdev, temporal, tutorial
Top comments (0)