DEV Community

Cover image for Goodbye Date: Why JavaScript is finally getting dates right.
Abdul Halim
Abdul Halim

Posted on

Goodbye Date: Why JavaScript is finally getting dates right.

If you've written JavaScript for a while, you've probably fought with Date at some point. It's one of the oldest parts of the language, barely changed since 1995, and honestly, it shows. Let's look at why it causes so much pain and how the new Temporal API fixes it, with real, tested examples.

Date is mutable, and that bites people constantly.

Date objects can be changed in place. That sounds fine until you pass one around your app and something quietly changes it behind your back.

const meeting = new Date('2024-03-01');
const followUp = meeting;
followUp.setMonth(followUp.getMonth() + 1);

console.log(meeting.toDateString()); // "Mon Apr 01 2024" (wait, the original changed too)
Enter fullscreen mode Exit fullscreen mode

You never touched meeting directly. But followUp is just a reference to the same object, so changing one changes both. This is a really easy mistake to make, and it tends to show up in bigger codebases where a date gets passed through a few layers of functions.

Adding months doesn't do what you'd expect.

const jan31 = new Date(2024, 0, 31); // Jan 31, 2024
jan31.setMonth(jan31.getMonth() + 1);

console.log(jan31.toDateString()); // "Sat Mar 02 2024" (not February at all)
Enter fullscreen mode Exit fullscreen mode

February only has 29 days that year, so Date just overflows into March instead of clamping to the end of the month. If you're building anything with recurring billing or subscription renewals, this kind of bug is waiting for you, usually right around the 29th, 30th, or 31st.

Parsing strings is a bit of a gamble.

console.log(new Date('01/02/2024').toDateString());
// "Tue Jan 02 2024" (read as MM/DD/YYYY)
Enter fullscreen mode Exit fullscreen mode

Is that January 2nd or February 1st? Depends on the browser, the locale, and the exact string. ISO strings like 2024-01-02 are safer, but plenty of real input in the wild isn't in ISO format, and Date doesn't give you a good way to say "parse this the way I mean."

It mixes up three different ideas.

Date treats these three things as if they were the same thing:

  • a calendar date, like "March 1st"
  • a wall-clock time, like "3:00 PM"
  • an exact moment in time, tied to a specific place

A birthday doesn't have a timezone. A 3 PM meeting only makes sense once you know whose 3 PM you mean. But under the hood, Date always stores a single number (milliseconds since the Unix epoch) and quietly converts through whatever timezone your machine happens to be running in. That's a pretty easy way to introduce a bug that only shows up once your app hits a server in a different timezone.

And speaking of timezones, Date doesn't really support them. You get UTC, and you get "whatever timezone this computer is set to," and nothing in between. Anything more than that means reaching for a library like luxon or date-fns-tz.

So what's Temporal?

Temporal is a modern date and time API for JavaScript, built to replace Date outright. It has officially reached TC39 Stage 4, making it a permanent part of the standard ECMAScript specification. It is now supported natively across major modern runtimes, including Chrome, Firefox, and Node.js. You can still ensure full cross-environment compatibility today by using production-ready packages like @js-temporal/polyfill.

The big idea is that instead of one object trying to be everything, you get separate types for separate concepts:

  • Temporal.PlainDate: just a calendar date, no time, no timezone
  • Temporal.PlainTime: just a time, no date
  • Temporal.PlainDateTime: date and time together, still no timezone
  • Temporal.ZonedDateTime: an exact moment, tied to a real timezone
  • Temporal.Instant: an exact moment on the UTC timeline
  • Temporal.Duration: a length of time, like "2 hours 30 minutes"

Let's see how this actually plays out.

It doesn't let you mutate by accident.

const meeting = Temporal.PlainDate.from('2024-03-01');
const followUp = meeting.add({ months: 1 });

console.log(meeting.toString());   // "2024-03-01" (untouched)
console.log(followUp.toString());  // "2024-04-01" (a brand new object)
Enter fullscreen mode Exit fullscreen mode

add() always hands back a new value. The original is never touched, so there's nothing to accidentally break further down the call stack.

Month math actually makes sense.

const jan31 = Temporal.PlainDate.from('2024-01-31');
const nextMonth = jan31.add({ months: 1 });

console.log(nextMonth.toString()); // "2024-02-29"
Enter fullscreen mode Exit fullscreen mode

Since February doesn't have a 31st, Temporal clamps to the last real day of that month instead of spilling over into March. (2024 is a leap year, so it lands on the 29th rather than the 28th, nice.)

Parsing is boring, in a good way.

Temporal only parses ISO 8601 by default, so there's no guessing between MM/DD and DD/MM:

const date = Temporal.PlainDate.from('2024-01-02');
console.log(date.toString()); // "2024-01-02" (always year-month-day)
Enter fullscreen mode Exit fullscreen mode

If you need to handle something like "1 Feb 2024", you convert it explicitly yourself, rather than hoping the engine guesses right.

Timezones are handled properly, DST and all.

This is probably the best part. Here's a time in New York, right as Daylight Saving kicks in:

const beforeDST = Temporal.ZonedDateTime.from(
  '2024-03-10T01:30:00[America/New_York]'
);
console.log(beforeDST.toString());
// "2024-03-10T01:30:00-05:00[America/New_York]"

const oneHourLater = beforeDST.add({ hours: 1 });
console.log(oneHourLater.toString());
// "2024-03-10T03:30:00-04:00[America/New_York]"
Enter fullscreen mode Exit fullscreen mode

A couple of things happen here: the offset flips from -05:00 to -04:00 on its own, and the clock jumps from 1:30 to 3:30 (a full two hours) because 2:00 to 2:59 simply doesn't exist that morning in New York. Temporal knows that and handles it correctly by default. With plain Date, getting this right takes careful manual offset math; here it just works.

Durations aren't an afterthought either.

const projectStart = Temporal.PlainDate.from('2024-06-01');
const projectEnd = Temporal.PlainDate.from('2024-06-15');

const span = projectStart.until(projectEnd);
console.log(span.days); // 14

const deadline = Temporal.Duration.from({ hours: 26 });
console.log(deadline.toString()); // "PT26H"
Enter fullscreen mode Exit fullscreen mode

until() gives you back a real Duration object, and you can ask it for .days, .hours, or a total in whatever unit you want, no dividing by 86400000 by hand.

Should you actually use it right now?

Browser support is rolling out, but it's not everywhere yet, so for now you'll probably want the polyfill:

npm install @js-temporal/polyfill
Enter fullscreen mode Exit fullscreen mode
import { Temporal } from '@js-temporal/polyfill';
Enter fullscreen mode Exit fullscreen mode

Worth checking caniuse.com/temporal before you decide whether you need it for your target environment.

Bottom line

Date isn't going anywhere. It's too deeply baked into the platform to remove. But for new code, Temporal is just a better tool: it doesn't mutate behind your back, it's upfront about timezones, and it doesn't have the silent overflow bugs that have probably caused a production incident or two somewhere. If you've ever chased down a mystery bug involving a date that was off by a month or an hour, Temporal was built with exactly that headache in mind.

Keep building, keep vibing!

Top comments (0)