How to Migrate from Moment.js to date-fns or Day.js
How do I migrate from Moment.js to date-fns or Day.js?
TL;DR
- Bottom line: Moment.js is officially in maintenance mode ("done, not dead"). Replace it with date-fns v4.3 (functional, tree-shakeable, ~13 KB tree-shaken; ~40M weekly downloads) or Day.js 1.11.x (chainable Moment-like API, ~2 KB core; ~25M weekly downloads) — both are immutable and actively maintained.
- Key tool/command:
npm install date-fnsornpm install dayjs— then find-and-replace Moment patterns using the Quick Reference table below. - Watch out for: Format token differences — Moment uses
YYYY-MM-DDbut date-fns usesyyyy-MM-dd(lowercase year/day). Day.js uses the same tokens as Moment. - Works with: date-fns v4.3.0 (Node 18+, all modern browsers), Day.js 1.11.21 (Node 12+, all browsers including IE11 with polyfills).
- Future: The Temporal API reached TC39 Stage 4 at the March 2026 plenary and ships natively in Chrome 144 (Jan 2026) and Firefox 139 (May 2025); Node.js v24 exposes it behind a flag. Still not cross-browser safe (Safari lags). Plan for Temporal as the long-term target; use date-fns or Day.js today.
Constraints
- date-fns v4.x requires Node.js 18+ — v3.x works with Node 16+, v2.x with Node 12+
- Never use Moment.js format tokens (
YYYY,DD) in date-fns — they produce silent wrong output or throw errors - Never let date-fns and Moment.js mutate the same Date object in the same scope — complete one migration file at a time
- Day.js plugins must be registered with
dayjs.extend()before calling plugin features — unregistered calls throw TypeError - date-fns wildcard import (
import * as dateFns) defeats tree-shaking — always use named imports - Temporal API (Chrome 144+, Firefox 139+) is not yet cross-browser safe — do not use as a Moment.js replacement in production targeting Safari or older browsers
Quick Reference
| Moment.js Pattern | date-fns Equivalent | Day.js Equivalent |
|---|---|---|
moment() | new Date() | dayjs() |
moment('2026-02-23') | parseISO('2026-02-23') | dayjs('2026-02-23') |
moment('12-25-2026', 'MM-DD-YYYY') | parse('12-25-2026', 'MM-dd-yyyy', new Date()) | dayjs('12-25-2026', 'MM-DD-YYYY') (customParseFormat plugin) |
moment().format('YYYY-MM-DD') | format(new Date(), 'yyyy-MM-dd') | dayjs().format('YYYY-MM-DD') |
moment().format('MMMM Do, YYYY') | format(new Date(), 'MMMM do, yyyy') | dayjs().format('MMMM Do, YYYY') (advancedFormat plugin) |
moment().add(7, 'days') | addDays(new Date(), 7) | dayjs().add(7, 'day') |
moment().subtract(1, 'month') | subMonths(new Date(), 1) | dayjs().subtract(1, 'month') |
moment().startOf('month') | startOfMonth(new Date()) | dayjs().startOf('month') |
moment().endOf('month') | endOfMonth(new Date()) | dayjs().endOf('month') |
moment().diff(other, 'days') | differenceInDays(new Date(), other) | dayjs().diff(other, 'day') |
moment().isBefore(other) | isBefore(new Date(), other) | dayjs().isBefore(other) |
moment().isAfter(other) | isAfter(new Date(), other) | dayjs().isAfter(other) |
moment().isSame(other, 'day') | isSameDay(new Date(), other) | dayjs().isSame(other, 'day') |
moment().fromNow() | formatDistance(new Date(), Date.now(), { addSuffix: true }) | dayjs().fromNow() (relativeTime plugin) |
moment().isValid() | isValid(parseISO(str)) | dayjs(str).isValid() |
Decision Tree
START
├── Need a Moment.js-compatible chainable API?
│ ├── YES → Use Day.js (near-identical API, 2 KB, plugin-based features)
│ └── NO ↓
├── Need tree-shaking / minimal bundle size?
│ ├── YES → Use date-fns (import only the functions you need, best tree-shaking)
│ └── NO ↓
├── Need first-class time zone support built in?
│ ├── YES → Use date-fns v4 (@date-fns/tz) or Day.js (utc + timezone plugins)
│ └── NO ↓
├── Migrating a large codebase with many moment() calls?
│ ├── YES → Day.js (smallest API surface change — often just rename import)
│ └── NO ↓
├── Prefer functional programming style (no method chaining)?
│ ├── YES → Use date-fns (pure functions, immutable by design)
│ └── NO ↓
├── Targeting only Chrome 144+ / Firefox 139+?
│ ├── YES → Consider Temporal API (native, zero-dependency) with date-fns/Day.js fallback
│ └── NO ↓
└── DEFAULT → date-fns for new projects, Day.js for quick drop-in replacements
Decision Logic
If the team wants the least painful drop-in with a Moment-compatible chainable API
--> Migrate to Day.js 1.11.x — identical format tokens and method names mean most call sites only need the import renamed; add plugins (customParseFormat, advancedFormat, relativeTime, utc, timezone) for parity. [src4, src9]
If client-side bundle size is the primary driver
--> Migrate to date-fns v4.3 with named imports (~13 KB tree-shaken for a typical app) or Day.js core (~2 KB + opt-in plugins) — both eliminate Moment's ~72 KB gzipped footprint. [src3, src9]
If the project needs first-class IANA time-zone support
--> Use date-fns v4 with @date-fns/tz (TZDate) or Day.js with the utc + timezone plugins; test across DST boundaries against the old moment.tz() output before removing Moment. [src3, src4]
If the codebase is large (>500 Moment call sites)
--> Choose Day.js to minimize the API-surface change, build a thin utils/date.js adapter layer first, and migrate file-by-file behind the adapter so the library can be swapped again later. [src5, src7]
If you prefer functional, immutable, TypeScript-first code with no method chaining
--> Use date-fns v4.3 (pure functions, ~40M weekly downloads, full TS types) — never expect mutation; always capture the returned Date. [src3, src6, src9]
If the deployment targets only Chrome 144+ / Firefox 139+ (or Node.js v24 with the flag)
--> Consider the native Temporal API (now TC39 Stage 4) for new code with a date-fns/Day.js fallback; do NOT ship Temporal to Safari or older browsers without the ~40 KB @js-temporal/polyfill. [src8, src9]
If the project is in maintenance mode and bundle size is not a concern
--> Leave Moment.js in place — the Moment team explicitly supports existing production code; migration is optional, not urgent. [src1]
Step-by-Step Guide
1. Audit Moment.js usage in your codebase
Quantify the migration scope by counting Moment.js call sites. This determines whether Day.js (minimal changes) or date-fns (more refactoring, better long-term) is the right target. [src1, src2]
# Count total moment imports/requires
grep -rn "require('moment')\|from 'moment'\|import moment" --include='*.js' --include='*.ts' | wc -l
# Count format() calls (need token conversion for date-fns)
grep -rn '\.format(' --include='*.js' --include='*.ts' | grep -i moment | wc -l
# Count timezone and locale usage
grep -rn 'moment\.tz\|moment-timezone' --include='*.js' --include='*.ts' | wc -l
grep -rn 'moment\.locale\|\.locale(' --include='*.js' --include='*.ts' | wc -l
Verify: If timezone count > 0, plan for @date-fns/tz or dayjs/plugin/timezone. If locale count > 0, plan for locale imports.
2. Install the replacement library
Install date-fns or Day.js alongside Moment.js. Both can coexist during the transition. [src3, src4]
# Option A: date-fns
npm install date-fns
npm install @date-fns/tz # if timezone support needed
# Option B: Day.js
npm install dayjs
Verify: node -e "const { format } = require('date-fns'); console.log(format(new Date(), 'yyyy-MM-dd'))" prints today's date.
3. Create a date utility wrapper
Build a thin adapter layer so the rest of your codebase imports from one file. This isolates the library choice and simplifies future migrations (e.g., to the Temporal API). [src7]
// utils/date.js — adapter layer
import { format, parseISO, addDays, subDays, differenceInDays,
isBefore, isAfter, startOfMonth, endOfMonth } from 'date-fns';
export const formatDate = (date, fmt = 'yyyy-MM-dd') => format(date, fmt);
export const parseDate = (str) => parseISO(str);
export const addDaysTo = (date, n) => addDays(date, n);
export const daysDiff = (a, b) => differenceInDays(a, b);
Verify: Import and call each exported function in a test file to confirm behavior matches your old Moment.js usage.
4. Convert format tokens (date-fns only)
Moment.js and date-fns use different format tokens. Day.js uses the same tokens as Moment, so skip this step if using Day.js. [src2, src6]
// Moment.js → date-fns format token conversion
// YYYY → yyyy (4-digit year)
// YY → yy (2-digit year)
// DD → dd (day of month, zero-padded)
// D → d (day of month)
// Do → do (ordinal day: 1st, 2nd, 3rd)
// dddd → EEEE (full weekday name)
// ddd → EEE (abbreviated weekday name)
// A → a (AM/PM)
// X → t (Unix timestamp seconds)
// x → T (Unix timestamp milliseconds)
Verify: For each format string in your codebase, compare moment(date).format(oldFmt) output with dateFns.format(date, newFmt).
5. Replace Moment.js calls with the new library
Systematically replace Moment calls file by file. Start with utilities and shared code, then work outward to UI components. [src2, src5]
// BEFORE: Moment.js
import moment from 'moment';
const formatted = moment().format('YYYY-MM-DD');
const nextWeek = moment().add(7, 'days');
// AFTER: date-fns
import { format, addDays } from 'date-fns';
const formatted = format(new Date(), 'yyyy-MM-dd');
const nextWeek = addDays(new Date(), 7);
// AFTER: Day.js
import dayjs from 'dayjs';
const formatted = dayjs().format('YYYY-MM-DD');
const nextWeek = dayjs().add(7, 'day');
Verify: Run your test suite after each file. Compare formatted output strings between old and new implementations.
6. Handle timezone and locale conversions
If your project uses moment-timezone or locale-specific formatting, add the corresponding packages. [src3, src4]
// date-fns v4: timezone support
import { TZDate } from '@date-fns/tz';
import { format } from 'date-fns';
const nyDate = new TZDate(2026, 1, 23, 12, 0, 0, 'America/New_York');
format(nyDate, 'yyyy-MM-dd HH:mm zzz');
// date-fns: locale support
import { de } from 'date-fns/locale';
format(new Date(), 'EEEE, d. MMMM yyyy', { locale: de });
// Day.js: timezone + locale
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc); dayjs.extend(timezone);
dayjs().tz('America/New_York').format('YYYY-MM-DD HH:mm z');
Verify: Compare timezone-converted output with Moment-timezone results across DST boundaries.
7. Remove Moment.js from dependencies
Once all usages are replaced and tests pass, uninstall Moment.js. [src1]
npm uninstall moment moment-timezone
# Verify no remaining references
grep -rn "moment" --include='*.js' --include='*.ts' --include='*.tsx' --include='*.json' | grep -v node_modules | grep -v '.md'
Verify: Build succeeds, all tests pass, npm ls moment shows no dependencies. Check bundle size reduction.
Code Examples
JavaScript: date-fns migration with full feature coverage
Full script: javascript-date-fns-migration-with-full-feature-co.js (34 lines)
// Input: A utility module that wraps all common Moment.js operations
// Output: The same module rewritten with date-fns v4
import {
format, parseISO, parse,
addDays, addMonths, addYears, subDays, subMonths,
differenceInDays, differenceInMonths,
isBefore, isAfter, isSameDay, isValid,
startOfMonth, endOfMonth, startOfWeek, endOfWeek,
formatDistance
} from 'date-fns';
// Parse ISO string (replaces moment('2026-02-23'))
const date = parseISO('2026-02-23');
// Parse custom format (replaces moment('02/23/2026', 'MM/DD/YYYY'))
const customParsed = parse('02/23/2026', 'MM/dd/yyyy', new Date());
// Format (replaces moment().format('MMMM Do, YYYY'))
const formatted = format(new Date(), 'MMMM do, yyyy'); // "February 23rd, 2026"
// Add/subtract (replaces moment().add(7, 'days'))
const nextWeek = addDays(new Date(), 7);
const lastMonth = subMonths(new Date(), 1);
// Difference (replaces moment(a).diff(b, 'days'))
const daysUntil = differenceInDays(parseISO('2026-12-31'), new Date());
// Comparison (replaces moment(a).isBefore(b))
const isPast = isBefore(parseISO('2025-01-01'), new Date()); // true
// Range boundaries (replaces moment().startOf('month'))
const monthStart = startOfMonth(new Date());
// Relative time (replaces moment().fromNow())
const relative = formatDistance(parseISO('2026-01-01'), new Date(), {
addSuffix: true
}); // "about 2 months ago"
// Validation (replaces moment(str).isValid())
const valid = isValid(parseISO('2026-02-30')); // false
JavaScript: Day.js migration with plugins
Full script: javascript-day-js-migration-with-plugins.js (36 lines)
// Input: A codebase using Moment.js with timezone and relative time
// Output: The same functionality using Day.js with required plugins
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import advancedFormat from 'dayjs/plugin/advancedFormat';
import relativeTime from 'dayjs/plugin/relativeTime';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import isBetween from 'dayjs/plugin/isBetween';
// Register plugins (one-time setup)
dayjs.extend(customParseFormat);
dayjs.extend(advancedFormat);
dayjs.extend(relativeTime);
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(isBetween);
// Parse (identical to Moment.js)
const date = dayjs('2026-02-23');
const custom = dayjs('02/23/2026', 'MM/DD/YYYY'); // requires customParseFormat
// Format (identical tokens to Moment.js)
const formatted = date.format('MMMM Do, YYYY'); // "February 23rd, 2026"
// Add/subtract (returns new instance — immutable unlike Moment)
const nextWeek = dayjs().add(7, 'day');
// Difference (note: singular unit name)
const daysUntil = dayjs('2026-12-31').diff(dayjs(), 'day');
// Relative time (requires relativeTime plugin)
const relative = dayjs('2026-01-01').fromNow();
// Timezone (requires utc + timezone plugins)
const nyTime = dayjs().tz('America/New_York').format('YYYY-MM-DD HH:mm z');
TypeScript: Type-safe date utility with date-fns
Full script: typescript-type-safe-date-utility-with-date-fns.ts (36 lines)
// Input: Need a type-safe date utility layer for a TypeScript project
// Output: Strongly typed wrapper around date-fns functions
import {
format, parseISO, addDays, subDays,
differenceInDays, isBefore, isAfter, isValid
} from 'date-fns';
type DateInput = Date | string;
function toDate(input: DateInput): Date {
if (typeof input === 'string') {
const parsed = parseISO(input);
if (!isValid(parsed)) {
throw new Error(`Invalid date string: ${input}`);
}
return parsed;
}
return input;
}
function formatDate(input: DateInput, pattern: string = 'yyyy-MM-dd'): string {
return format(toDate(input), pattern);
}
function daysBetween(a: DateInput, b: DateInput): number {
return differenceInDays(toDate(a), toDate(b));
}
function isDateBefore(a: DateInput, b: DateInput): boolean {
return isBefore(toDate(a), toDate(b));
}
// Usage:
const result = formatDate('2026-02-23', 'MMMM do, yyyy'); // "February 23rd, 2026"
const diff = daysBetween('2026-12-31', '2026-02-23'); // 311
const past = isDateBefore('2025-01-01', new Date()); // true
Anti-Patterns
Wrong: Expecting mutation like Moment.js
// ❌ BAD — date-fns returns new Date objects, does NOT mutate
const date = new Date('2026-02-23');
addDays(date, 7); // Return value discarded!
console.log(date); // Still 2026-02-23, not 2026-03-02
Correct: Capture the return value
// ✅ GOOD — Always assign the result
import { addDays } from 'date-fns';
const date = new Date('2026-02-23');
const nextWeek = addDays(date, 7); // New Date: 2026-03-02
console.log(nextWeek); // 2026-03-02
console.log(date); // 2026-02-23 (unchanged)
Wrong: Using Moment.js format tokens with date-fns
// ❌ BAD — Moment tokens produce wrong output in date-fns
import { format } from 'date-fns';
format(new Date(), 'YYYY-MM-DD');
// Throws error or wrong output — YYYY and DD are not valid date-fns tokens
Correct: Use date-fns format tokens
// ✅ GOOD — date-fns uses lowercase year/day tokens
import { format } from 'date-fns';
format(new Date(), 'yyyy-MM-dd'); // "2026-02-23"
// Key: YYYY→yyyy, DD→dd, dddd→EEEE, Do→do, A→a
Wrong: Importing all of date-fns (kills tree-shaking)
// ❌ BAD — Imports entire library (~70 KB)
import * as dateFns from 'date-fns';
dateFns.format(new Date(), 'yyyy-MM-dd');
Correct: Named imports for tree-shaking
// ✅ GOOD — Tree-shakeable, only ~3-6 KB
import { format, addDays, parseISO } from 'date-fns';
format(new Date(), 'yyyy-MM-dd');
Wrong: Using Day.js features without loading plugins
// ❌ BAD — fromNow() is not in Day.js core
import dayjs from 'dayjs';
dayjs().fromNow(); // TypeError: dayjs(...).fromNow is not a function
Correct: Extend Day.js with required plugins first
// ✅ GOOD — Load plugins before using their features
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
dayjs().fromNow(); // "a few seconds ago"
Wrong: Assuming Day.js mutates like Moment.js
// ❌ BAD — Day.js is immutable
const d = dayjs('2026-02-23');
d.add(7, 'day');
console.log(d.format('YYYY-MM-DD')); // Still "2026-02-23"!
Correct: Chain or capture the new Day.js instance
// ✅ GOOD — Day.js returns new instances
const d = dayjs('2026-02-23');
const nextWeek = d.add(7, 'day');
console.log(nextWeek.format('YYYY-MM-DD')); // "2026-03-02"
Common Pitfalls
- Format token confusion (date-fns): Moment uses
YYYYfor year, date-fns usesyyyy. Using Moment tokens in date-fns causes errors or wrong output. Fix: find-and-replaceYYYY→yyyy,DD→dd,dddd→EEEE,Do→do,A→a. [src2] - Missing Day.js plugins: Day.js core is minimal — features like
fromNow(), custom parsing, timezone,isBetween(), and ordinals require plugins. Fix: import anddayjs.extend()each plugin at app startup. [src4] - Moment.js mutation assumption: Moment modifies objects in place. Both date-fns and Day.js are immutable. Fix: always assign the return value. [src5, src6]
- Bundle size regression with wildcard import:
import * as dateFns from 'date-fns'imports the entire library (~70 KB), negating tree-shaking. Fix: use named imports. [src3] - Timezone handling differences: Moment-timezone bundles tz data. date-fns v4 uses
@date-fns/tzwithTZDate; Day.js usesdayjs/plugin/timezone. Fix: test across DST boundaries and convert moment.tz() calls individually. [src3, src4] - Locale loading differences: Moment bundles all locales (~300 KB). date-fns and Day.js require explicit locale imports. Fix:
import { de } from 'date-fns/locale'orimport 'dayjs/locale/de'. [src2] - parseISO vs new Date(): date-fns
parseISO()strictly parses ISO 8601 only.new Date(str)gives inconsistent results for non-ISO strings. Fix: useparse()with explicit format for non-ISO strings. [src3] - Day.js diff() uses singular units: Moment uses
diff(other, 'days')(plural), Day.js usesdiff(other, 'day')(singular). Fix: use singular unit names. [src4]
Diagnostic Commands
# Check Moment.js version and bundle impact
npm ls moment moment-timezone
npx bundlephobia moment # ~290 KB minified, ~72 KB gzipped
# Find all Moment.js imports
grep -rn "require('moment')\|from 'moment'" --include='*.js' --include='*.ts' | wc -l
# Find format strings needing token conversion
grep -rn "\.format(" --include='*.js' --include='*.ts' | grep -v node_modules
# Check date-fns bundle size (tree-shaken: typically 3-15 KB)
npx bundlephobia date-fns
# Check Day.js bundle size (~2.9 KB gzipped core)
npx bundlephobia dayjs
# Verify no Moment.js references remain
grep -rn "moment" --include='*.js' --include='*.ts' --include='*.tsx' | grep -v node_modules | grep -v '.md'
# Check if Temporal API is available in your Node.js version
node -e "console.log(typeof Temporal !== 'undefined' ? 'Temporal available' : 'Temporal not available')"
Version History & Compatibility
| Library | Version | Status | Key Features | Node.js |
|---|---|---|---|---|
| Moment.js 2.x | Maintenance only | Legacy — no new features | Mutable API, bundled locales | Any |
| date-fns 4.3.0 | Current (latest v4.x) | Active | First-class TZ via @date-fns/tz, ESM-first, TZ in format/formatISO/formatRFC3339, Temporal JSDoc refs | 18+ |
| date-fns 4.1.0 | Sep 2024 | Maintained | Added TZ to format/formatISO/formatRelative | 18+ |
| date-fns 3.x | Previous (Dec 2023) | Maintained | TypeScript rewrite, ESM + CJS dual | 16+ |
| date-fns 2.x | Legacy | Bug fixes only | Original API, separate date-fns-tz | 12+ |
| Day.js 1.11.21 | Current (May 2026) | Active, stable (since 2018) | Moment-compatible API, plugin system, ESM support | 12+ |
| Temporal API | TC39 Stage 4 (Mar 2026) | Chrome 144 (Jan 2026), Firefox 139 (May 2025), Node.js v24 (flag) | Native date/time, immutable, timezone-aware | Stabilizing |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Codebase uses Moment.js and needs bundle size reduction | Simple date formatting (1-2 operations) | Native Intl.DateTimeFormat |
| Starting a new project needing date manipulation | Application targets very old browsers without build tools | Moment.js (still works, just large) |
| Need immutable date operations to prevent bugs | Project is in maintenance mode with no new development | Leave Moment.js in place |
| Tree-shaking important for client-side performance | Need complete ISO 8601 / RFC 2822 parser | Luxon or Temporal (future) |
| Want functional style (choose date-fns) | Team prefers chainable object API | Choose Day.js instead |
| Want Moment-compatible API (choose Day.js) | Need advanced calendar systems (Hebrew, Islamic) | Luxon or Intl API |
| Targeting only Chrome 144+ / Firefox 139+ | Need cross-browser support including Safari | Temporal API (with polyfill) |
Important Caveats
- Moment.js is not "broken" — if your project works and bundle size is not a concern, migration is optional. The Moment team explicitly says existing production code can stay.
- date-fns v4 has breaking changes in timezone handling compared to v3's
date-fns-tzpackage. The v4.1 release added timezone support toformat,formatISO,formatISO9075,formatRelative, andformatRFC3339. If upgrading from v3, review the @date-fns/tz migration guide separately. - Day.js is largely Moment-compatible but not 100% — edge cases exist around DST transitions, locale-specific ordinals, and strict parsing. The 1.11.x line (current 1.11.21, May 2026) has ESM module support and improved timezone plugin millisecond precision. Always test date-heavy logic after migration.
- The Temporal API reached TC39 Stage 4 at the March 2026 plenary — it is now an approved part of the language. It ships natively in Chrome 144 (January 2026) and Firefox 139 (May 2025); Node.js v24 exposes it behind a flag and Safari support is partial. Temporal will eventually supersede all third-party date libraries, but cross-browser production use still requires
@js-temporal/polyfill(~40 KB). date-fns has an "Interim API" experiment underway to offer a Temporal-compatible build alongside itsDateAPI. - Both date-fns and Day.js use native JavaScript Date objects internally, inheriting Date's UTC vs local time gotchas. Be explicit about timezone expectations.