Dates & Time
Dates & Time

Date libraries: when, which, and where Temporal stands

Card · ~10 min
After this unit you can
  • Recall which jobs Date + Intl already cover, and which justify a library: wall-clock arithmetic in a named zone, durations, many input formats
  • Recall how date-fns v4 (plain functions on Date, in the machine's zone unless you pass in: tz(…)) differs from Luxon (an immutable DateTime that carries its zone)
  • Recall Temporal's types (Instant, ZonedDateTime, PlainDate) and its status in October 2026: Stage 4, shipped in Chrome/Edge 144, Firefox 139 and Node 26, not yet in Safari
  • Recall why Moment.js is not for new code: a legacy project in maintenance mode, with a mutable API

Cards

The built-ins cover most jobs

Flag

Before adding a dependency, check what the job actually is:

JobBuilt-in
Store or send an instantDate + toISOString()
Show an instant to a personIntl.DateTimeFormat with timeZone
"3 days ago"Intl.RelativeTimeFormat (you compute the number)
Date-only math (due dates, birthdays)~20 lines of UTC helpers
"Today" for a userformatToParts with their timeZone

A library earns its place when the job is wall-clock arithmetic in a named zone (a meeting at 09:00 New York time every Monday, across DST), durations and intervals ("2 months, 3 days"), or parsing many custom formats. Date can't do arithmetic in any zone except the machine's and UTC, and those are exactly the cases where hand-rolled code goes wrong.

date-fns: plain functions on Date

Flag

date-fns (v4, current 4.4) is a box of small functions on Date. They never mutate their input, and you import only what you use. The catch: a Date has no zone, so date-fns calculates in the machine's zone unless you give it one. Since v4, that's the in option with tz() from @date-fns/tz, and a date it returns is then a TZDate, a Date subclass that keeps the zone:

import { addDays, format } from "date-fns";
import { tz } from "@date-fns/tz";

const standUp = new Date("2026-03-07T14:00:00Z");     // 09:00 in New York
const ny = tz("America/New_York");
format(addDays(standUp, 1, { in: ny }), "yyyy-MM-dd HH:mm", { in: ny }); // "2026-03-08 09:00"

addDays(standUp, 1); // 14:00 UTC on a UTC server, 13:00 UTC on a New York laptop

Without in, the same line gives different instants on different machines across a DST change.

Luxon: a DateTime that carries its zone

Flag

Luxon (v3, from the Moment team) wraps an instant and a zone in an immutable DateTime. It gets zone rules from the Intl API, so it ships no time zone data. Its docs separate two kinds of math: calendar math for days, weeks, months and years (keeps the wall-clock time) and time math for hours and smaller (adds exact milliseconds).

import { DateTime } from "luxon";

const standUp = DateTime.fromISO("2026-03-07T09:00", { zone: "America/New_York" });
standUp.plus({ days: 1 }).toISO();   // "2026-03-08T09:00:00.000-04:00": still 09:00
standUp.plus({ hours: 24 }).toISO(); // "2026-03-08T10:00:00.000-04:00": the 23-hour day
standUp.setZone("Asia/Tokyo").toISO(); // "2026-03-07T23:00:00.000+09:00": same instant

Pick Luxon when values naturally belong to a place: schedules, bookings, anything "at 09:00 in New York".

Temporal: one type per kind of value

Flag

Temporal is the built-in replacement for Date, and its main idea is separate types for the separate things this track has been telling apart:

  • Temporal.Instant: an exact moment, no zone (what Date really is).
  • Temporal.ZonedDateTime: an instant plus an IANA zone; day arithmetic keeps the wall clock across DST.
  • Temporal.PlainDate: a date-only value. No time, no zone, no off-by-one.
  • PlainDateTime, PlainTime, Duration, and Temporal.Now for "now" and "today".
Temporal.PlainDate.from("2026-02-28").add({ days: 1 }).toString(); // "2026-03-01"
Temporal.PlainDate.from("2026-02-30");                             // throws RangeError
Temporal.ZonedDateTime.from("2026-03-07T09:00[America/New_York]")
  .add({ days: 1 }).toString(); // "2026-03-08T09:00:00-04:00[America/New_York]"
Temporal.Now.plainDateISO("Asia/Tokyo");                           // today, in Tokyo

Objects are immutable, and invalid strings throw instead of rolling over.

Where Temporal stands (October 2026)

Flag
  • Standard: Stage 4 at TC39, listed for ES2027.
  • Shipped: Firefox 139 (May 2025), Chrome and Edge 144 (January 2026), Node.js 26 (May 2026), Deno 2.7, Bun 1.4.
  • Not shipped: Safari, which has it only in Technology Preview. So MDN marks Temporal "Limited availability", not Baseline. Node 24 LTS doesn't have it either.

MDN's Date page already calls Date "a legacy feature" and suggests Temporal for new code, "check the browser compatibility" included.

In practice: server code on Node 26+ can use it today. Browser code that must work in Safari needs a polyfill (the proposal lists temporal-polyfill as stable and @js-temporal/polyfill as alpha) or a feature check:

const hasTemporal = typeof globalThis.Temporal !== "undefined";

Recheck MDN's compatibility table before you drop the polyfill.

Not Moment, for new code

Flag

Moment.js still has millions of downloads, and coding assistants still suggest it. Its own team calls it "a legacy project" in maintenance mode (project status update, August 2026): no new features, and no redesign of its mutable API. The 2020 status post it replaced asked new projects to choose something else, also citing bundle size, since Moment doesn't tree-shake.

Mutation is the bug it's famous for:

const start = moment("2026-03-10");
const end = start.add(7, "days"); // add() changes start and returns it
start.format("YYYY-MM-DD");       // "2026-03-17": the start date moved
end === start;                    // true

For new code use the built-ins, date-fns, Luxon or Temporal. In existing code, Moment isn't an emergency; migrate the parts you touch, and clone (start.clone().add(…)) until then.

Picking one

Flag
You needReach for
An instant in an API, log or databaseDate, sent as toISOString()
An instant shown to a personIntl.DateTimeFormat with the person's timeZone
Due dates, birthdays, holidaysUTC helpers, or Temporal.PlainDate where it ships
09:00 every Monday in New YorkLuxon, date-fns + @date-fns/tz, or Temporal.ZonedDateTime
Durations, intervals, many input formatsLuxon or Temporal
A new projectanything here except Moment

Whatever you pick, the rules from this track don't change: store instants in UTC, keep calendar dates as date-only values, name the zone wherever a person's wall clock matters, and run the tests in a zone that isn't UTC.

Further reading