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.

LibraryModelImmutableZones
Date (built-in JS)One mutable instant typeNoOffset only, no zone database
date-fnsFunctions over plain DateYes (returns new values)Via date-fns-tz
Day.jsMutable wrapper, Moment-likeNo (clone needed)Via plugin, needs tz data
LuxonImmutable DateTime, Duration, IntervalYesFull IANA via Intl
Temporal (JS)Separate types per conceptYesFull IANA, built in
java.timeLocalDate, Instant, ZonedDateTime, DurationYesFull IANA via ZoneId
Python datetime + zoneinfoNaive and aware datetimesNoIANA database from the system
Go timeValue type holding an instant and a locationYes (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 duration

Choosing 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 of pytz for new code — pytz requires the unusual localize call and compares badly with the standard API.
  • In Java, never use java.util.Date or Calendar in 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.

Daylight saving and its edge cases Internationalisation: locales, calendars and relative time

Last refreshed 2026-09-18.