Temporal API to fix JavaScript Date

Ákos (heu)Ákos (heu)
5 min read

Let's get one thing out of the way: working with JavaScript dates is an absolute nightmare.

The Date API is unwieldy, to say the least, and immutability is almost non-existent. This was more than enough to nudge most developers to third-party libraries like Luxon (remember Moment.js?) and date-fns to streamline the experience of working with date and time.

These third party libraries might not be needed anymore. Let’s take a look at the new Temporal object, with all its improvements over Date.

Issues with the Date object

Some of the biggest issues are that it only supports UTC and local (machine) time, with no time zone support. This also means that daylight savings aren’t really respected, as they are tied to a specific time zone.

Parsing a date from a string is also quite error prone, and the Date object is also mutable, which is its own can of worms.

As a last issue, only the Gregorian calendar is supported by the Date implementation.

What does Temporal do?

Citing the MDN docs: “It is designed as a full replacement for the Date object.

It adds time zone support, 200+ methods that make working with dates much more straight forward, and more explicit date parsing/creation.

As the API is quite broad, it might take a bit of getting used to, but in the end, it should be a native replacement for most (if not all) date-related third party libraries.

To mention some added benefits: Temporal is globally available (just like Math, there’s no need to import it), and everything that it exposes is immutable. You can say goodbye to those esoteric bugs that were introduced by overwriting Date's behavior.

Temporal data types

There are two main categories when it comes to the Temporal API:

  • Plain - denoting a date or time devoid of any time zone information

  • Zoned - representing a date or time paired with a time zone

Each one of these has its own use case. We might use a PlainDateTime when we only care about a specific time with no regards to the time zone, while a ZonedDateTime is a better fit when we want to do any operations (like adding/substracting hours or calculating the difference between dates) that need to take the current time zone (and DST) into consideration.

Another new concept would be Duration, which represents the difference between two dates (like C#’s TimeSpan).

Knowing this, it’s easy to tell at a glance what each of the new classes is supposed to represent:

  1. Temporal.Duration - difference between two time points

  2. Temporal.Instant - unique point in history (represented as the number of nanoseconds since the Unix epoch)

  3. Temporal.Now - provides methods to get the current time in various formats

  4. Temporal.PlainDate - calendar date; no time or tz

  5. Temporal.PlainDateTime - calendar date and time; no tz

  6. Temporal.PlainMonthDay - calendar month + day; no year or tz

  7. Temporal.PlainTime - time without date or tz

  8. Temporal.PlainYearMonth - calendar year + month; no day or tz

  9. Temporal.ZonedDateTime - date and time with a tz; represented as a combination of an instant, tz and a calendar system

There are of course conversion methods as well between these classes.

Methods representing operations

Some often-encountered requirements are “adding/substracting X days to a date” or “calculating days left until X”. Comparing dates has also been a challenge, not to mention serialization to the proper format.

All use cases above have new methods in each of the mentioned classes.

Let’s take a look at some often encountered scenarios, starting with plain/zoned ISO times:

const now = Temporal.Now.plainDateTimeISO();
console.log(now.toString());
// 2025-01-28T18:48:02.118476193

const nowNewYork = Temporal.Now.plainDateTimeISO("America/New_York");
console.log(nowNewYork.toString());
// 2025-01-28T12:48:21.966577614

A couple manipulation methods highlighted below as well:

add/subtract

The two methods above are identical when it comes to usage: pass in a temporalDurationLike (basically an object specifying the number of years/months/days/hours/mins/etc).

//  Adding
const now = Temporal.Now.plainDateTimeISO();
console.log(now.toString()); // 2025-01-28T18:52:35.647752417

const sameTimeNextMonth = now.add({ months: 1 });
console.log(sameTimeNextMonth.toString()); // 2025-02-28T18:52:35.647752417

// Subtracting
const sameTimePrevMonth = now.subtract({ months: 1 });
console.log(sameTimePrevMonth.toString()); // 2024-12-28T18:52:35.647752417

since/until

As above, identical in usage: pass in the point of comparison, and an optional options object, containing the keys largestUnit, roundingIncrement, roundingMode, and smallestUnit.

As working with Unix timestamps is quite common, let’s use one as our starting point:

const lastUpdated = Temporal.Instant.fromEpochMilliseconds(1735235418000);
const now = Temporal.Now.instant();

const duration = now.since(lastUpdated, { smallestUnit: "minute" });
console.log(`Last updated ${duration.toLocaleString("en-US")} ago`);
// Last updated 47,529 min ago

const christmas = Temporal.Instant.fromEpochMilliseconds(1766664000000);
const timespan = now.until(christmas, { smallestUnit: "hour" });
console.log(`There are ${timespan.toLocaleString("en-US")}s until Christmas!`);

equals

Compares two Temporal.Instant instances, and returns true if the two represent the same point in time:

const nowString = Temporal.Instant.from("2025-01-28T19:13:04.145+01:00[Europe/Budapest]");
const nowMs = Temporal.Instant.fromEpochMilliseconds(1738087984145);
console.log(instant1.equals(instant2)); // true

Trying out the Temporal API

Currently no major browsers ship with the Temporal API, but most browsers include it in their canary/nightly builds. As of now, Firefox has it enabled in the nightly version by default, Safari has it behind the --use-temporal flag, and Chrome (as all V8 engines) uses the --harmony-temporal flag.

If you’re not keen on setting up canary builds of different browsers, there are two other paths you can take: using the @js-temporal/polyfill directly, or accessing the Temporal proposal docs which has the polyfill set up and available, so you can mess around with the new API in the devtools of your browser.

Closing thoughts

While the new APIs aren’t quite yet available, support is slowly but surely landing in all modern browsers. Given the robustness of the API and the wide range of friction points it addresses, it’s fair to say that it’ll be the way to go when it comes to any kind of date/time manipulation in JavaScript - be that on the frontend or in a Node.js app/backend.

Should we go ahead and use the new API with the help of the polyfill? Probably not.

The roadmap on NPM clearly states that it is not production ready yet. If you’re feeling particularly open for some risk, you could always adopt the new way of working in an isolated, non-”business critical” part of your application (although I wouldn’t persist it in a database, as things might still change). This would give the developers on your team a head start on familiarizing themselves with the new API.

Don’t forget to explore the huge amount of documentation and examples that MDN provides on the Temporal API and all of its bits and pieces.

Some links to the proposal/cookbook for further reading:

  1. Temporal Proposal

  2. The Temporal Cookbook Examples

0
Subscribe to my newsletter

Read articles from Ákos (heu) directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ákos (heu)
Ákos (heu)