date-fns vs dayjs vs moment vs luxon vs datejs
Date/Time Handling in Modern Frontend Apps
date-fnsdayjsmomentluxondatejsSimilar Packages:
Date/Time Handling in Modern Frontend Apps

Date/time libraries help JavaScript apps handle parsing, formatting, arithmetic, and time zones in a predictable way. The native Date object is mutable, tied to the runtime’s local time zone by default, and inconsistent across environments when parsing strings. These libraries provide higher-level primitives and consistent APIs so developers can reason about calendars, time zones, locales, and edge cases like daylight saving transitions.

date-fns offers a pure-function toolbox centered on Date objects. dayjs, luxon, and moment wrap dates in objects with chainable methods; dayjs and luxon are immutable while moment is mutable. datejs predates the others and patches native prototypes; it’s effectively obsolete for modern production use.

Npm Package Weekly Downloads Trend
3 Years
Github Stars Ranking
Stat Detail
Package
Downloads
Stars
Size
Issues
Publish
License
date-fns35,949,66036,28122.6 MB861a year agoMIT
dayjs30,733,26648,384679 kB1,16315 days agoMIT
moment25,990,31048,0894.35 MB3032 years agoMIT
luxon17,051,57916,2284.59 MB1812 months agoMIT
datejs21,064354-3811 years agoMIT

Picking the Right Clock: date-fns vs datejs vs dayjs vs luxon vs moment

Dates are easy—until you ship across time zones, translate to multiple locales, hit the end of February, or cross a daylight saving boundary. These libraries exist to make string parsing predictable, math repeatable, and formatting consistent so UI and business logic match user expectations.

Utility Functions vs. Wrapped Instances vs. Patched Prototypes

There are three distinct models here:

  • date-fns: a collection of pure functions that operate on native Date values. No wrappers, no globals, no mutation of prototypes.
  • dayjs/luxon/moment: object wrappers with chainable methods. dayjs and luxon are immutable (each call returns a new instance). moment is mutable by default, which affects how you manage state.
  • datejs: augments native prototypes and changes the behavior of Date. That was convenient in the past, but clashes with modern expectations (predictability, isolation, TypeScript, tree-shaking).

Why Mutability Changes How You Write React and Server Code

  • date-fns and luxon/dayjs encourage safe patterns: you get a new value after every operation. Accidental shared-state bugs are less likely.
  • moment mutates in place unless you call .clone(). That’s quick in scripts but risky in shared objects or React state.
  • datejs modifies the built-in Date prototype; operations can surprise you when libraries assume the native behavior.

Time Zones: Built-In vs. Plugin vs. Not Supported

  • luxon has first-class IANA time zone handling via Intl. DateTime objects can carry a zone; conversions are explicit and reliable.
  • dayjs supports time zones through the utc and timezone plugins. Once enabled, dayjs.tz(...) works well for cross-zone math.
  • moment handles zones with the moment-timezone add-on.
  • date-fns does not handle IANA zone conversions in core. You can compute with native Date (tied to system/local/offset) or add date-fns-tz for zone conversion/formatting.
  • datejs does not provide reliable, modern IANA time zone support.

The Same Task, Five Ways: Convert, Add, Format Across a DST Boundary

Task: Take an ISO string with offset, convert to Europe/Paris, add 1 hour, format as YYYY-MM-DD HH:mm (or equivalent).

const iso = '2020-03-29T01:30:00+01:00'; // DST change in Europe

// date-fns (no IANA time zone support in core)
import { addHours, format } from 'date-fns';
const d = new Date(iso);
const plus1 = addHours(d, 1);
// NOTE: This formats in the runtime's local zone, not 'Europe/Paris'.
console.log(format(plus1, 'yyyy-MM-dd HH:mm'));
// For proper zone handling, use companion package `date-fns-tz`.

// dayjs (with timezone plugin)
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);
const djs = dayjs.tz(iso, 'Europe/Paris').add(1, 'hour');
console.log(djs.format('YYYY-MM-DD HH:mm'));

// luxon (built-in zones)
import { DateTime } from 'luxon';
const l = DateTime.fromISO(iso, { setZone: true }).setZone('Europe/Paris').plus({ hours: 1 });
console.log(l.toFormat('yyyy-LL-dd HH:mm'));

// moment (with moment-timezone)
import moment from 'moment-timezone';
const m = moment.tz(iso, 'Europe/Paris').add(1, 'hour');
console.log(m.format('YYYY-MM-DD HH:mm'));

// datejs (no reliable IANA zone support)
// Datejs patches Date; typical code cannot convert to a named IANA zone.
// You’ll end up with local time unless you compute offsets manually.
var dj = new Date(iso);
// Adding an hour mutates or returns a new Date depending on datejs usage; zone conversion is not supported.
var plus1dj = new Date(dj.getTime() + 60 * 60 * 1000);
// Formatting often relies on datejs's extended toString patterns if loaded:
// console.log(dj.toString('yyyy-MM-dd HH:mm'));

Observable differences:

  • luxon/dayjs/moment let you set the zone directly and handle DST transitions for that zone.
  • date-fns requires an extra package for zones; core functions operate on Date in the current environment’s zone or specified offsets.
  • datejs lacks modern zone support, so accurate cross-zone formatting isn’t practical.

Month Math and End-of-Month Behavior is Not the Same Everywhere

Adding one month to January 31 should clamp to the last valid day in February for calendar math.

// date-fns
import { addMonths } from 'date-fns';
addMonths(new Date(2021, 0, 31), 1); // -> Feb 28, 2021 (clamped)

// dayjs
dayjs('2021-01-31').add(1, 'month'); // -> 2021-02-28

// luxon
DateTime.fromISO('2021-01-31').plus({ months: 1 }); // -> 2021-02-28

// moment
moment('2021-01-31').add(1, 'month'); // -> 2021-02-28

// datejs (prototype-patched Date; behavior depends on datejs API)
// Common usage historically:
// Date.parse('2021-01-31').add({ months: 1 }); // Often clamped to Feb 28; API reliability varies by build

These libraries converge on clamping behavior, but the route is different: date-fns does it functionally, dayjs/luxon/moment do it on instances, while datejs depends on prototype-augmented methods that aren’t consistently maintained.

Parsing: Strict Tokens Beat Loose Strings

  • date-fns provides parse with explicit tokens and a reference date; parseISO is available for ISO strings.
  • dayjs needs customParseFormat for token-based parsing; otherwise it prefers ISO and RFC 2822.
  • luxon has fromISO and fromFormat with strict tokens and locale/zone options.
  • moment supports moment(str, fmt, strict) and has years of parsing behavior people rely on.
  • datejs historically attempted natural-language parsing; it’s not aligned with modern strict-token approaches and can vary by build.

Example of strict parsing (European day-first):

const input = '02/01/2024 23:15'; // dd/MM/yyyy HH:mm

// date-fns
import { parse, isValid } from 'date-fns';
const d = parse(input, 'dd/MM/yyyy HH:mm', new Date());
console.log(isValid(d));

// dayjs (customParseFormat plugin)
import customParseFormat from 'dayjs/plugin/customParseFormat';
dayjs.extend(customParseFormat);
const djs = dayjs(input, 'DD/MM/YYYY HH:mm', true); // strict
console.log(djs.isValid());

// luxon
const l = DateTime.fromFormat(input, 'dd/LL/yyyy HH:mm', { zone: 'UTC', locale: 'en' });
console.log(l.isValid);

// moment
const m = moment(input, 'DD/MM/YYYY HH:mm', true);
console.log(m.isValid());

// datejs
// Historically Date.parseExact existed for strict token parsing, but availability varies by build/version.
// This is not reliable in modern setups without specific datejs bundles.

Localization Without Surprises

  • date-fns formats with a locale passed per call: format(date, pattern, { locale }). No global state.
  • dayjs sets locale globally or per instance depending on usage; locale data is loaded on demand.
  • luxon leans on Intl; setLocale can be applied to a DateTime, and toLocaleString gives high-quality outputs.
  • moment uses moment.locale(...) and ships many locales; it’s global by default but can be scoped per instance.
  • datejs provided culture files historically; not aligned with modern Intl and build tooling.
// Example: French formatting of a date
// date-fns
import { format } from 'date-fns';
import { fr } from 'date-fns/locale';
format(new Date(2024, 6, 14), 'd MMMM yyyy', { locale: fr });

// dayjs
import 'dayjs/locale/fr';
dayjs.locale('fr');
dayjs('2024-07-14').format('D MMMM YYYY');

// luxon
DateTime.fromISO('2024-07-14').setLocale('fr').toLocaleString({ day: 'numeric', month: 'long', year: 'numeric' });

// moment
moment('2024-07-14').locale('fr').format('D MMMM YYYY');

Relative Time and Durations: Who Has What

  • Relative time: date-fns has formatDistanceToNow, dayjs has a relativeTime plugin, luxon offers toRelative(), moment has fromNow(). datejs does not provide a modern relative-time API.
  • Durations/intervals: luxon includes Duration and Interval types; moment has moment.duration; dayjs has a duration plugin; date-fns returns numbers for differences and works with plain objects; datejs’s approach predates these patterns.
// Relative time, 90 minutes from now in the past
const dPast = new Date(Date.now() - 90 * 60 * 1000);

// date-fns
import { formatDistanceToNow } from 'date-fns';
formatDistanceToNow(dPast, { addSuffix: true }); // "about 1 hour ago"

// dayjs
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
dayjs(dPast).fromNow(); // "an hour ago"

// luxon
DateTime.fromJSDate(dPast).toRelative(); // "1 hour ago" (approx)

// moment
moment(dPast).fromNow(); // "an hour ago"

// datejs
// No built-in modern relative time; would require custom logic.

TypeScript and Build-Time Behavior Matter

  • date-fns is modular and tree-shakable; types are straightforward function signatures. No global state.
  • dayjs has strong typings; plugins add types when extended. Instances are immutable, which fits reducer-style state.
  • luxon ships TypeScript types and leverages Intl; the object model clearly separates DateTime, Duration, and Interval.
  • moment types exist but the mutable API can lead to accidental in-place changes that types won’t prevent.
  • datejs is not TypeScript-centered and clashes with ESM/modern bundlers.

Summary Table

Dimensiondate-fnsdatejsdayjsluxonmoment
Core modelPure functions on DatePatches native DateImmutable wrapperImmutable wrapperMutable wrapper
Time zone supportNo IANA zones in core (use companion)Not modern IANAPlugin (utc, timezone)Built-in via IntlAdd-on (moment-timezone)
ParsingToken-based functionsPrototype-augmented, inconsistentISO by default; plugin for tokensISO and token-basedFlexible with strict mode
DST handlingAccurate with correct zone (needs tz helper)Not reliableAccurate with timezone pluginAccurate built-inAccurate with timezone add-on
LocalizationPer-call locale optionLegacy culturesLocales via importIntl-basedGlobal or per-instance locale
Relative timeYes (formatDistanceToNow)No modern APIPlugin (relativeTime)Built-in (toRelative)Built-in (fromNow)
Durations/IntervalsNumbers/objects; no classesLegacy patternsPlugin (duration)Built-in Duration/IntervalBuilt-in duration
MutabilityNone (stateless)Mutates prototypesImmutableImmutableMutable (clone to avoid)
Plugin modelNot plugin-basedN/AFocused pluginsMinimal need for pluginsMany add-ons
Prototype modificationNoYesNoNoNo
TypeScriptFirst-class typesPoor/legacyFirst-class typesFirst-class typesTypes available
Bundle impactPer-function importsGlobal patchingSmall core + opt-in pluginsSingle modern moduleMonolithic core + add-ons

Across these libraries, the pattern shows a split between functional utilities (date-fns) and immutable object wrappers (dayjs, luxon), with moment representing legacy mutability and datejs reflecting an older era of prototype patching.

How to Choose: date-fns vs dayjs vs moment vs luxon vs datejs
  • date-fns:

    Choose date-fns if you want small, tree-shakable utilities with no global state or object wrappers. It works directly with native Date and excels at formatting, arithmetic, and interval math. It lacks built-in time zone support (use a companion like date-fns-tz if you need named zones) and does not provide first-class Duration/Interval objects. This approach fits functional programming styles and keeps dependencies minimal.

  • dayjs:

    Choose dayjs if you want a small, Moment-like API with immutable instances and a focused plugin system. It handles most everyday tasks cleanly, and plugins cover features like time zones, relative time, duration, and advanced parsing. Its behavior is predictable in frameworks where immutability matters (e.g., React state). You’ll need to opt into plugins for non-core features, but that also keeps surface area clear and intentional.

  • moment:

    moment is a legacy project in maintenance mode and should not be used for new projects. Consider it when maintaining existing codebases that already rely on its mutable API, extensive plugin ecosystem, and broad community knowledge. For time zone support you’ll typically pair it with moment-timezone. For new work, evaluate dayjs, luxon, or date-fns instead.

  • luxon:

    Choose luxon when you need first-class time zone handling, ISO-first APIs, and robust Duration/Interval types built in. It relies on the platform’s Intl support for zones and locales and offers strong, immutable DateTime objects with clear semantics. It’s well-suited for apps that do serious scheduling, cross-zone conversions, and locale-aware formatting without extra plugins. The API is modern and consistent, favoring explicit configuration over magical defaults.

  • datejs:

    Avoid datejs for new projects. It extends native prototypes, has long been unmaintained, and behavior can vary across environments. While it once offered friendly parsing and formatting helpers, it does not align with modern JavaScript practices or tooling. Prefer actively maintained alternatives that support time zones, TypeScript, and modern module workflows.

README for date-fns

🔥️ NEW: date-fns v4.0 with first-class time zone support is out!

date-fns

date-fns provides the most comprehensive, yet simple and consistent toolset for manipulating JavaScript dates in a browser & Node.js

👉 Documentation

👉 Blog


It's like Lodash for dates

  • It has 200+ functions for all occasions.
  • Modular: Pick what you need. Works with webpack, Browserify, or Rollup and also supports tree-shaking.
  • Native dates: Uses existing native type. It doesn't extend core objects for safety's sake.
  • Immutable & Pure: Built using pure functions and always returns a new date instance.
  • TypeScript: The library is 100% TypeScript with brand-new handcrafted types.
  • I18n: Dozens of locales. Include only what you need.
  • and many more benefits
import { compareAsc, format } from "date-fns";

format(new Date(2014, 1, 11), "yyyy-MM-dd");
//=> '2014-02-11'

const dates = [
  new Date(1995, 6, 2),
  new Date(1987, 1, 11),
  new Date(1989, 6, 10),
];
dates.sort(compareAsc);
//=> [
//   Wed Feb 11 1987 00:00:00,
//   Mon Jul 10 1989 00:00:00,
//   Sun Jul 02 1995 00:00:00
// ]

The library is available as an npm package. To install the package run:

npm install date-fns --save

Docs

See date-fns.org for more details, API, and other docs.


License

MIT © Sasha Koss