date-fns
A collection of 200+ pure functions for working with native JavaScript Date objects: formatting, parsing, math, comparisons, and intervals. There is no wrapper class; every function takes a Date and returns a new Date, so it drops into any codebase that already passes Dates around. It is fully tree-shakeable (you pay only for the functions you import), written in TypeScript, ships dozens of opt-in locales, and since v4 has first-class time zone support through the companion @date-fns/tz package.
The default pick for date work in modern JavaScript: native Dates, tree-shaking, and solid TypeScript types with no lock-in. Reach for Luxon instead when time zone logic dominates your domain.
Use it if
- You want tree-shakeable date helpers where importing format and addDays costs a few KB instead of shipping a whole date class
- Your code already passes native Date objects around and you do not want to convert to and from a wrapper type at every boundary
- You need localized formatting in only a few languages and want to bundle just the locales you import
- You need time zone aware dates but want them as an opt-in extra (@date-fns/tz) rather than a heavier all-in-one library
- Time zone arithmetic is central to your app; Luxon's DateTime carries its zone with it, which is harder to get wrong than juggling TZDate instances next to plain Dates
- You are parsing messy user-entered date strings; parse() demands an exact format token string and there is no fuzzy or best-effort parsing
- Your team prefers a chainable object API; 200+ loose functions mean a lot of import statements and a discovery problem, and Day.js reads more like the Moment style they may know
- You need rich Duration and Interval types with ISO 8601 period formatting; date-fns covers basic distances and intervals but Luxon goes further
Setup reality
npm install date-fns and you are done: zero dependencies, no peer deps, no config. The annoyances are elsewhere. The package went ESM-first in v3 and import paths changed across v2 to v3 to v4, so a lot of older Stack Overflow answers show stale deep-import styles. Locales are not automatic: you import each one and pass it to every format call that needs it. Time zones live in the separate @date-fns/tz package, which is easy to miss when you search the main docs. Format tokens follow the Unicode standard (yyyy, dd), so muscle memory from Moment (YYYY, DD) produces wrong output that still looks plausible.
Patterns
Format a date as a stringformat-date
import { format } from "date-fns";
format(new Date(2026, 7, 4), "yyyy-MM-dd");
// => "2026-08-04"
format(new Date(), "EEEE, MMMM do");
// => "Tuesday, August 4th"Tokens are Unicode style: yyyy and dd, not Moment's YYYY and DD. YYYY means week-numbering year and will silently give wrong results around New Year.
Parse an ISO 8601 stringparse-iso-string
import { parseISO } from "date-fns";
const d = parseISO("2026-08-04T10:30:00Z");Use parseISO for ISO strings instead of new Date(); it has consistent cross-browser behavior for partial dates.
Parse a string with a known formatparse-custom-format
import { parse, isValid } from "date-fns";
const d = parse("04/08/2026", "dd/MM/yyyy", new Date());
if (!isValid(d)) throw new Error("bad date");The third argument is a reference date for missing parts. A failed parse returns an Invalid Date object, not an error, so always check isValid.
Add or subtract days, months, hoursadd-subtract-time
import { addDays, subMonths, addHours } from "date-fns";
const due = addDays(new Date(), 14);
const lastQuarter = subMonths(new Date(), 3);
const later = addHours(new Date(), 6);Every function returns a new Date and never mutates its input.
Difference between two datesdifference-between-dates
import { differenceInDays, differenceInMinutes } from "date-fns";
differenceInDays(new Date(2026, 7, 10), new Date(2026, 7, 4));
// => 6Results are truncated toward zero, not rounded: 47 hours apart is 1 day, not 2.
Compare and sort datescompare-and-sort
import { compareAsc, isBefore, isAfter } from "date-fns";
dates.sort(compareAsc);
isBefore(a, b); // true if a < bcompareAsc plugs straight into Array.prototype.sort; use compareDesc for newest-first.
Human relative time (3 days ago)relative-time
import { formatDistanceToNow } from "date-fns";
formatDistanceToNow(post.createdAt, { addSuffix: true });
// => "3 days ago"Without addSuffix you get just "3 days", which reads wrong in most UIs.
Start or end of a day, month, weekstart-end-of-period
import { startOfDay, endOfMonth, startOfWeek } from "date-fns";
const dayStart = startOfDay(new Date());
const monthEnd = endOfMonth(new Date());
const weekStart = startOfWeek(new Date(), { weekStartsOn: 1 });Weeks start on Sunday by default; pass weekStartsOn: 1 for Monday or a locale that implies it.
Every day in a rangeiterate-date-range
import { eachDayOfInterval } from "date-fns";
const days = eachDayOfInterval({
start: new Date(2026, 7, 1),
end: new Date(2026, 7, 7),
});
// => array of 7 DatesThe interval is inclusive on both ends; it throws RangeError if start is after end.
Is a date inside a rangecheck-within-interval
import { isWithinInterval } from "date-fns";
isWithinInterval(new Date(), {
start: promo.startsAt,
end: promo.endsAt,
});Boundaries are inclusive; for exclusive checks combine isAfter and isBefore yourself.
Format in another languagelocalized-format
import { format } from "date-fns";
import { fr } from "date-fns/locale";
format(new Date(), "PPPP", { locale: fr });
// => "mardi 4 aout 2026"Locales are opt-in imports, so only the ones you import end up in your bundle; there is no global locale setting by default (setDefaultOptions exists if you want one).
Work in a specific time zone (v4+)timezone-aware-date
import { TZDate } from "@date-fns/tz";
import { addHours, format } from "date-fns";
const kolkata = new TZDate(2026, 7, 4, 9, 0, "Asia/Kolkata");
format(addHours(kolkata, 4), "HH:mm zzz");TZDate lives in the separate @date-fns/tz package (npm install @date-fns/tz); all date-fns functions accept it because it extends Date.