JavaScript Finally Gets a Real Date API

When has working with dates ever been easy? Every language has its own version of the same headaches: time zones, parsing, leap years, arithmetic that does weird things at month boundaries. JavaScript just had some quirks layered on top, extra cruft left over from when the language was first created. Third party libraries like Moment.js or date-fns had to fill in the gaps.

Those days are over. Now we have the Temporal API.

Why Date Was Broken

Let me give you the short version of why Date is the way it is. It was inspired by Java’s java.util.Date from the 90s, which Java itself eventually deprecated. JavaScript inherited the design and never let it go.

The problems are well-known at this point:

Mutability. Pass a Date to a function and that function can change it underneath you.

const myDate = new Date('2023-01-01');
function addDays(date, days) {
  date.setDate(date.getDate() + days);
  return date;
}
addDays(myDate, 5);
console.log(myDate.toISOString().slice(0, 10)); // '2023-01-06'. Surprise, your original is gone.

Time zone confusion. Date stores milliseconds since the Unix epoch but formats itself in the user’s local time zone. Working in any other zone means reaching for moment-timezone or date-fns-tz.

Parsing roulette. new Date("2023-01-01") and new Date("Jan 1, 2023") can return different things depending on the browser and the assumed time zone.

Math that lies. Adding a month to January 31st? Date rolls it forward to March 3rd because February doesn’t have 31 days. That’s not a bug exactly. It’s just Date being honest that it doesn’t really understand calendars.

What Temporal Actually Fixes

Temporal is a new global object designed from the ground up to address all of this. The design choices are worth walking through because they’re opinionated in the right ways.

Everything is immutable

Every operation returns a new object. Your original data stays put.

const start = Temporal.PlainDate.from('2023-01-01');
const end = start.add({ days: 5 });

console.log(start.toString()); // '2023-01-01'
console.log(end.toString());   // '2023-01-06'

Different types for different concepts

This is the part I find most interesting. Date tries to be everything, a timestamp, a calendar date, a wall clock time, all at once. Temporal splits these into distinct types and forces you to pick:

  • Temporal.PlainDate: a calendar date, no time, no zone. Birthdays, anniversaries.
  • Temporal.PlainTime: a wall-clock time, no date.
  • Temporal.PlainDateTime: date and time, no zone.
  • Temporal.ZonedDateTime: fully zone-aware and calendar-aware. The one for global apps.
  • Temporal.Instant: an exact point in time, like epoch milliseconds.
  • Temporal.Duration: a length of time.

Making you pick the right type up front is the whole game. Half the bugs in date code come from pretending a Date is one thing when it’s actually another.

Time zones and calendars built in

Temporal natively understands IANA time zones (America/New_York, Europe/Paris) and non-Gregorian calendars (Hebrew, Islamic, Japanese). No external library needed.

Math that respects the calendar

const t = Temporal.PlainDate.from('2023-01-31');
const nextMonth = t.add({ months: 1 });
console.log(nextMonth.toString()); // '2023-02-28'

It clamps to the end of the month instead of rolling over. That’s almost always what you actually wanted.

Comparisons and Diffs

A couple of quick ones, because these are the operations you do constantly.

Comparing two dates:

const t1 = Temporal.PlainDate.from('2023-01-01');
const t2 = Temporal.PlainDate.from('2023-01-01');
console.log(Temporal.PlainDate.compare(t1, t2) === 0); // true

No more getTime() dance to compare primitives. There’s an actual comparison function.

Finding the difference:

const start = Temporal.PlainDate.from('2023-01-01');
const end = Temporal.PlainDate.from('2023-12-31');
const diff = start.until(end, { largestUnit: 'days' });
console.log(diff.days); // 364

No more dividing milliseconds by 1000 * 60 * 60 * 24 and hoping DST doesn’t mess you up.

Should You Use It Yet?

Check your runtime. Browser and Node support has been landing, but you’ll want to verify Temporal is available where you’re shipping, or use the official polyfill while you wait.

For most date and time work, this replaces Moment.js and date-fns entirely. Moment has been in maintenance mode for years. Temporal gives you the good parts of those libraries as a standard, immutable, well-typed API.

Date will stick around forever for backwards compatibility. But for new code, use Temporal. The API is better, the semantics are saner, and less bug-prone.

I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].

/ Programming / javascript / Web development