Date libraries compared
date-fns and Luxon, Day.js and the Moment migration path, java.time, Python datetime with zoneinfo, and Go's time package — with the mental model each one imposes.
Two mental models
Libraries split into two camps: mutable date objects with implicit conversion, and separate types per concept. The second camp prevents a whole class of bugs by making the mistake a type error.
| Library | Model | Immutable | Zones |
|---|---|---|---|
| Date (built-in JS) | One mutable instant type | No | Offset only, no zone database |
| date-fns | Functions over plain Date | Yes (returns new values) | Via date-fns-tz |
| Day.js | Mutable wrapper, Moment-like | No (clone needed) | Via plugin, needs tz data |
| Luxon | Immutable DateTime, Duration, Interval | Yes | Full IANA via Intl |
| Temporal (JS) | Separate types per concept | Yes | Full IANA, built in |
| java.time | LocalDate, Instant, ZonedDateTime, Duration | Yes | Full IANA via ZoneId |
| Python datetime + zoneinfo | Naive and aware datetimes | No | IANA database from the system |
| Go time | Value type holding an instant and a location | Yes (values) | IANA database, may be embedded |
The same task in each
// Luxon: intent is visible in the type
import { DateTime } from "luxon";
const d = DateTime.fromISO("2026-09-18T10:30:00Z", { zone: "Europe/Paris" });
d.plus({ months: 1 }).toISO(); // calendar arithmetic
d.plus({ days: 30 }).toISO(); // exact duration
// date-fns: functional, tree-shakeable
import { addMonths, format } from "date-fns";
format(addMonths(new Date("2026-01-31T00:00:00Z"), 1), "yyyy-MM-dd"); // 2026-02-28
// Day.js: Moment-compatible surface, plugin for zones
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
dayjs.extend(utc);
dayjs.utc("2026-09-18T10:30:00Z").add(1, "month").format();import java.time.*;
import java.time.format.DateTimeFormatter;
Instant instant = Instant.parse("2026-09-18T10:30:00Z");
ZonedDateTime paris = instant.atZone(ZoneId.of("Europe/Paris"));
LocalDate dateOnly = LocalDate.of(2026, 9, 18); // no time, no zone
paris.plusMonths(1).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
Duration.ofMinutes(90); // exact
Period.ofMonths(1); // calendar
// never use SimpleDateFormat in new code: not thread-safe, and legacy zone handling
ZonedDateTime parsed = ZonedDateTime.parse("2026-09-18T10:30:00+02:00[Europe/Paris]");now := time.Now() // has a monotonic reading
loc, _ := time.LoadLocation("Europe/Paris")
local := now.In(loc)
t, err := time.Parse(time.RFC3339, "2026-09-18T10:30:00Z") // strict
if err != nil { log.Fatal(err) }
_ = t.AddDate(0, 1, 0) // calendar arithmetic (year, month, day)
_ = t.Add(30 * 24 * time.Hour) // exact durationChoosing and migrating
- Moment.js is in maintenance mode and is not the right choice for new code; Day.js is a near drop-in replacement with a much smaller bundle.
- Luxon and date-fns are the current mainstream picks in JavaScript; Temporal will supersede most of the need once widely available.
- In Python, use
zoneinfo(3.9+) instead ofpytzfor new code —pytzrequires the unusuallocalizecall and compares badly with the standard API. - In Java, never use
java.util.DateorCalendarin new code. - Always keep the tz database current: a stale copy means wrong offsets after a government change.
# pytz (legacy) vs zoneinfo (modern)
import pytz
from zoneinfo import ZoneInfo
dt = datetime(2026, 9, 18, 10, 30)
legacy = pytz.timezone("Europe/Paris").localize(dt) # explicit localize step
modern = dt.replace(tzinfo=ZoneInfo("Europe/Paris")) # plain constructor
# both are aware, but only zoneinfo behaves like the standard library expects💡
The library matters less than the discipline: pick one per project, require aware values at every boundary, and forbid creating a naive datetime in application code. A single type checker or lint rule enforcing that prevents more bugs than any library choice.
FAQ
Is the built-in JavaScript Date good enough?
For UTC-only timestamps and simple formatting, yes. As soon as zones, calendar arithmetic or daylight saving are involved, it is not, and a library pays for itself immediately.
How big is the tz database?
The full IANA data is a few hundred kilobytes compressed and updates several times a year. Update it with your runtime or OS, and never freeze a copy into an application.
Related
Daylight saving and its edge cases Internationalisation: locales, calendars and relative time
Last refreshed 2026-09-18.