How to Migrate from Moment.js to date-fns or Day.js

Type: Software Reference Confidence: 0.93 Sources: 8 Verified: 2026-02-23 Freshness: quarterly

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') (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

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

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

LibraryVersionStatusKey FeaturesNode.js
Moment.js 2.xMaintenance onlyLegacy — no new featuresMutable API, bundled localesAny
date-fns 4.1.0Current (Sep 2024)ActiveFirst-class TZ via @date-fns/tz, ESM-first, TZ in format/formatISO/formatRFC333918+
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.19CurrentActive, stable (since 2018)Moment-compatible API, plugin system, ESM support12+
Temporal APIStage 3 TC39Chrome 144 (Jan 2026), Firefox 139 (May 2025)Native date/time, immutable, timezone-awareNot yet stable

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 important for client-side performanceNeed complete ISO 8601 / RFC 2822 parserLuxon or Temporal (future)
Want functional style (choose date-fns)Team prefers chainable object APIChoose 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 SafariTemporal API (with polyfill)

Important Caveats

Related Units