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

Constraints

Quick Reference

Moment.js Patterndate-fns EquivalentDay.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') (requires 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') (requires 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() (requires 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 (dayjs/plugin/utc + dayjs/plugin/timezone)
│   └── 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' --include='*.jsx' --include='*.tsx' | wc -l

# Count format() calls (need token conversion for date-fns)
grep -rn '\.format(' --include='*.js' --include='*.ts' | grep -i moment | wc -l

# Count locale usage (affects plugin choices)
grep -rn 'moment\.locale\|\.locale(' --include='*.js' --include='*.ts' | wc -l

# Count timezone usage
grep -rn 'moment\.tz\|moment-timezone' --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 (or both during a transition period). [src3, src4]

# Option A: date-fns (functional, tree-shakeable)
npm install date-fns
# For timezone support:
npm install @date-fns/tz

# Option B: Day.js (chainable, Moment-like API)
npm install dayjs

Verify: node -e "const { format } = require('date-fns'); console.log(format(new Date(), 'yyyy-MM-dd'))" prints today's date, or node -e "const dayjs = require('dayjs'); console.log(dayjs().format('YYYY-MM-DD'))".

3. Create a date utility wrapper (recommended)

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 subDaysFrom = (date, n) => subDays(date, n);
export const daysDiff = (a, b) => differenceInDays(a, b);
export const before = (a, b) => isBefore(a, b);
export const after = (a, b) => isAfter(a, b);
export const monthStart = (date) => startOfMonth(date);
export const monthEnd = (date) => endOfMonth(date);

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 — note: date-fns uses lowercase 'a')
// X    → t     (Unix timestamp in seconds)
// x    → T     (Unix timestamp in milliseconds)

// Common patterns:
// 'YYYY-MM-DD'        → 'yyyy-MM-dd'
// 'MM/DD/YYYY'        → 'MM/dd/yyyy'
// 'MMMM Do, YYYY'     → 'MMMM do, yyyy'
// 'dddd, MMMM D YYYY' → 'EEEE, MMMM d yyyy'
// 'hh:mm A'           → 'hh:mm a'
// 'HH:mm:ss'          → 'HH:mm:ss'     (same)

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 now = moment();
const formatted = now.format('YYYY-MM-DD');
const nextWeek = moment().add(7, 'days');
const diff = moment('2026-12-31').diff(moment(), 'days');

// AFTER: date-fns
import { format, addDays, differenceInDays, parseISO } from 'date-fns';

const now = new Date();
const formatted = format(now, 'yyyy-MM-dd');
const nextWeek = addDays(new Date(), 7);
const diff = differenceInDays(parseISO('2026-12-31'), new Date());

// AFTER: Day.js
import dayjs from 'dayjs';

const now = dayjs();
const formatted = now.format('YYYY-MM-DD');
const nextWeek = dayjs().add(7, 'day');
const diff = dayjs('2026-12-31').diff(dayjs(), '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: timezone support (v4+)
import { TZDate } from '@date-fns/tz';
import { format } from 'date-fns';

const nyDate = new TZDate(2026, 1, 23, 12, 0, 0, 'America/New_York');
console.log(format(nyDate, 'yyyy-MM-dd HH:mm zzz'));

// date-fns: locale support
import { format } from 'date-fns';
import { de } from 'date-fns/locale';
format(new Date(), 'EEEE, d. MMMM yyyy', { locale: de });

// Day.js: timezone support
import dayjs from 'dayjs';
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');

// Day.js: locale support
import 'dayjs/locale/de';
dayjs.locale('de');
dayjs().format('dddd, D. MMMM YYYY');

Verify: Compare timezone-converted output with the original Moment-timezone results for several known dates 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='*.jsx' --include='*.tsx' --include='*.json' | grep -v node_modules | grep -v '.md'

Verify: Build succeeds, all tests pass, and 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,
// ... (see full script)

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';
// ... (see full script)

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
// ... (see full script)

Anti-Patterns

Wrong: Using Moment.js mutability patterns with date-fns

// ❌ BAD — Expecting mutation like Moment.js
const date = new Date('2026-02-23');
addDays(date, 7);  // Returns a new Date, does NOT modify 'date'
console.log(date); // Still 2026-02-23, not 2026-03-02!

Correct: Capture the return value

// ✅ GOOD — date-fns returns new Date objects, 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 — immutable)

Wrong: Using Moment.js format tokens with date-fns

// ❌ BAD — Moment.js tokens in date-fns produce wrong output
import { format } from 'date-fns';
format(new Date(), 'YYYY-MM-DD');
// Throws error or produces unexpected 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 differences: YYYY→yyyy, DD→dd, dddd→EEEE, Do→do, A→a

Wrong: Importing all of date-fns

// ❌ BAD — Defeats the purpose of tree-shaking, imports entire library
import * as dateFns from 'date-fns';
dateFns.format(new Date(), 'yyyy-MM-dd');
// Bundle includes all 200+ functions (~70 KB)

Correct: Import only what you need

// ✅ GOOD — Tree-shakeable named imports
import { format, addDays, parseISO } from 'date-fns';
format(new Date(), 'yyyy-MM-dd');
// Bundle includes only format, addDays, parseISO (~3-6 KB)

Wrong: Using Day.js features without loading plugins

// ❌ BAD — fromNow() is not available without the relativeTime plugin
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: Chaining Day.js without realizing it returns new instances

// ❌ BAD — Assuming Day.js mutates like Moment.js
const d = dayjs('2026-02-23');
d.add(7, 'day');
console.log(d.format('YYYY-MM-DD')); // Still "2026-02-23"!
// Day.js is immutable — add() returns a new instance

Correct: Chain or capture the new instance

// ✅ GOOD — Day.js returns new instances, just like date-fns returns new Dates
const d = dayjs('2026-02-23');
const nextWeek = d.add(7, 'day');
console.log(nextWeek.format('YYYY-MM-DD')); // "2026-03-02"
// Or chain: dayjs('2026-02-23').add(7, 'day').format('YYYY-MM-DD')

Common Pitfalls

Diagnostic Commands

# Check current 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 in the project
grep -rn "require('moment')\|from 'moment'" --include='*.js' --include='*.ts' --include='*.tsx' | wc -l

# Find Moment.js format strings that need token conversion
grep -rn "\.format(" --include='*.js' --include='*.ts' | grep -v node_modules

# Check date-fns bundle size (tree-shaken)
npx bundlephobia date-fns  # Full: ~80 KB, but tree-shaking brings it to 3-15 KB typically

# Check Day.js bundle size
npx bundlephobia dayjs  # ~2.9 KB minified + gzipped (core only)

# Verify no Moment.js references remain after migration
grep -rn "moment" --include='*.js' --include='*.ts' --include='*.tsx' --include='*.jsx' | grep -v node_modules | grep -v "\.md" | grep -v "// moment"

# 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

LibraryVersionStatusKey FeaturesNode.js
Moment.js 2.xMaintenance onlyLegacy — no new features, no v3 plannedMutable API, bundled locales, moment-timezoneAny
date-fns 4.3.0Current (latest v4.x)ActiveFirst-class TZ via @date-fns/tz, ESM-first, format/formatISO/formatRFC3339 TZ support, Temporal JSDoc refs18+
date-fns 4.1.0Sep 2024MaintainedAdded TZ to format/formatISO/formatRelative18+
date-fns 3.xPrevious (Dec 2023)MaintainedTypeScript rewrite, ESM + CJS dual16+
date-fns 2.xLegacyBug fixes onlyOriginal API, separate date-fns-tz12+
Day.js 1.11.21Current (May 2026)Active, stable (since 2018)Moment-compatible API, plugin system, ESM support12+
Temporal APITC39 Stage 4 (Mar 2026)Chrome 144 (Jan 2026), Firefox 139 (May 2025), Node.js v24 (flag), Safari partialNative date/time, immutable, timezone-aware, no dependenciesStabilizing

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Codebase uses Moment.js and needs bundle size reductionSimple date formatting (1-2 operations)Native Intl.DateTimeFormat
Starting a new project needing date manipulationApplication targets very old browsers without build toolsMoment.js (still works, just large)
Need immutable date operations to prevent bugsProject is in maintenance mode with no new developmentLeave Moment.js in place
Tree-shaking is important for client-side performanceNeed a complete ISO 8601 / RFC 2822 parserLuxon or Temporal (future)
Want functional programming style (choose date-fns)Team prefers chainable object APIChoose Day.js instead
Want Moment-compatible API with minimal changes (choose Day.js)Need advanced calendar systems (Hebrew, Islamic)Luxon or Intl API
Targeting only Chrome 144+ / Firefox 139+Need cross-browser support including SafariTemporal API (with polyfill)

Important Caveats