---
# === IDENTITY ===
id: software/migrations/momentjs-to-datefns/2026
canonical_question: "How do I migrate from Moment.js to date-fns or Day.js?"
aliases:
  - "Moment.js to date-fns migration"
  - "replace Moment.js"
  - "Moment.js to Day.js"
  - "Moment.js alternatives migration"
  - "migrate from moment to date-fns"
  - "switch from Moment.js to Day.js"
  - "Moment.js replacement guide"
  - "moment.js date-fns migration 2026"
entity_type: software_reference
domain: software > migrations > momentjs_to_datefns
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-05-29
confidence: 0.93
version: 2.1
first_published: 2026-02-17

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: "date-fns v4.0 (2024-09-16)"
  next_review: 2026-11-25
  change_sensitivity: medium

# === CONSTRAINTS ===
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"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User only needs simple date formatting (1-2 operations) without Moment.js"
    use_instead: "Use native Intl.DateTimeFormat or Temporal API (if browser support allows)"
  - condition: "User wants to migrate to Luxon instead of date-fns or Day.js"
    use_instead: "Luxon migration guide — Luxon is heavier (~60 KB) but has built-in Intl and timezone support"
  - condition: "User is migrating between date-fns versions (v2 to v3 or v3 to v4)"
    use_instead: "date-fns changelog and migration guide at https://date-fns.org"

# === AGENT HINTS ===
inputs_needed:
  - key: target_library
    question: "Which replacement library do you prefer?"
    type: choice
    options: ["date-fns (functional, tree-shakeable)", "Day.js (chainable, Moment-like API)", "Not sure — help me decide"]
  - key: timezone_usage
    question: "Does your project use moment-timezone or timezone-aware date operations?"
    type: choice
    options: ["Yes — timezone support needed", "No — local dates only"]
  - key: codebase_size
    question: "How many Moment.js call sites are in your codebase?"
    type: choice
    options: ["Small (<50 calls)", "Medium (50-500 calls)", "Large (>500 calls)"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/migrations/momentjs-to-datefns/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-05-29)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/migrations/javascript-to-typescript/2026"
      label: "JavaScript to TypeScript Migration"
    - id: "software/migrations/webpack-to-vite/2026"
      label: "Webpack to Vite Migration"
  alternative_to:
    - id: "software/migrations/jquery-to-react/2026"
      label: "jQuery to React Migration"
  often_confused_with:
    - id: "software/migrations/javascript-to-typescript/2026"
      label: "JavaScript to TypeScript Migration"

# === SOURCES (9 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "Moment.js Project Status"
    author: Moment.js
    url: https://momentjs.com/docs/#/-project-status/
    type: official_docs
    published: 2020-09-01
    reliability: authoritative
  - id: src2
    title: "You Don't Need Moment.js"
    author: You-Dont-Need
    url: https://github.com/you-dont-need/You-Dont-Need-Momentjs
    type: community_resource
    published: 2024-01-15
    reliability: high
  - id: src3
    title: "date-fns v4.0 — First-class Time Zone Support"
    author: date-fns
    url: https://blog.date-fns.org/v40-with-time-zone-support/
    type: official_docs
    published: 2024-09-16
    reliability: authoritative
  - id: src4
    title: "Day.js Documentation"
    author: Day.js
    url: https://day.js.org/
    type: official_docs
    published: 2024-01-01
    reliability: authoritative
  - id: src5
    title: "Why We Switched from Moment.js to Day.js"
    author: ottonova
    url: https://ottonova.tech/why-we-switched-from-moment-js-to-day-js/
    type: technical_blog
    published: 2023-05-10
    reliability: high
  - id: src6
    title: "Migrating from Moment.js to Date-Fns: An Inevitable Choice"
    author: Oreate AI
    url: https://www.oreateai.com/blog/migrating-from-momentjs-to-datefns-an-inevitable-choice-for-modern-date-handling/b761efdbba2253ba9eca0b2ecb8b0dff
    type: technical_blog
    published: 2024-06-20
    reliability: moderate_high
  - id: src7
    title: "Migrate from Moment.js to date-fns"
    author: Sipos Daniel
    url: https://blog.siposdani87.com/posts/migrate-from-momentjs-to-date-fns
    type: technical_blog
    published: 2023-08-15
    reliability: moderate_high
  - id: src8
    title: "Temporal API Ships in Chrome 144"
    author: InfoQ
    url: https://www.infoq.com/news/2026/02/chrome-temporal-date-api/
    type: industry_report
    published: 2026-01-13
    reliability: high
  - id: src9
    title: "date-fns v4 vs Temporal API vs Day.js for JavaScript Date Handling (2026)"
    author: PkgPulse
    url: https://www.pkgpulse.com/guides/date-fns-v4-vs-temporal-api-vs-dayjs-date-handling-2026
    type: technical_blog
    published: 2026-04-10
    reliability: moderate_high
---

# 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. [src9]
- **Key tool/command**: `npm install date-fns` or `npm install dayjs` — then find-and-replace Moment patterns using the Quick Reference table below.
- **Watch out for**: Format token differences — Moment uses `YYYY-MM-DD` but date-fns uses `yyyy-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. It is still not cross-browser safe (Safari lags). Plan for Temporal as the long-term target; use date-fns or Day.js today. [src8, src9]

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

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

```bash
# 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]

```bash
# 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]

```javascript
// 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]

```javascript
// 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]

```javascript
// 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]

```javascript
// 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]

```bash
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](scripts/javascript-date-fns-migration-with-full-feature-co.js) (34 lines)

```javascript
// 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](scripts/javascript-day-js-migration-with-plugins.js) (36 lines)

```javascript
// 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](scripts/typescript-type-safe-date-utility-with-date-fns.ts) (36 lines)

```typescript
// 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

```javascript
// ❌ 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

```javascript
// ✅ 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

```javascript
// ❌ 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

```javascript
// ✅ 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

```javascript
// ❌ 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

```javascript
// ✅ 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

```javascript
// ❌ 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

```javascript
// ✅ 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

```javascript
// ❌ 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

```javascript
// ✅ 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

- **Format token confusion (date-fns)**: The most common migration bug. Moment uses `YYYY` for year, date-fns uses `yyyy`. Using Moment tokens in date-fns causes errors or wrong output. Fix: Use a find-and-replace for `YYYY`->`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 advanced format tokens (`Do`) all require separate plugins. Fix: Check the Day.js plugin docs; import and `dayjs.extend()` each plugin at app startup. [src4]
- **Moment.js mutation assumption**: Moment modifies objects in place (`date.add(1, 'day')` mutates `date`). Both date-fns and Day.js are immutable. Fix: Always assign the return value: `const newDate = addDays(date, 1)` or `const newDate = d.add(1, 'day')`. [src5, src6]
- **Bundle size regression with date-fns wildcard import**: Using `import * as dateFns from 'date-fns'` or the `/index.js` entry imports the entire library, negating the tree-shaking advantage. Fix: Use named imports: `import { format, addDays } from 'date-fns'`. [src3]
- **Timezone handling differences**: Moment-timezone stores timezone data in the library. date-fns v4 uses `@date-fns/tz` with `TZDate`, Day.js uses `dayjs/plugin/timezone`. Both use IANA timezone IDs but the APIs differ significantly. Fix: Test across DST boundaries; convert moment.tz() calls individually. [src3, src4]
- **Locale loading differences**: Moment bundles all locales by default (~300 KB). date-fns requires explicit locale imports passed as options. Day.js requires explicit locale file imports. Fix: For date-fns: `import { de } from 'date-fns/locale'; format(date, 'PP', { locale: de })`. For Day.js: `import 'dayjs/locale/de'; dayjs.locale('de')`. [src2]
- **parseISO vs new Date()**: date-fns's `parseISO()` strictly parses ISO 8601 strings only. Using `new Date(str)` for non-ISO strings can give inconsistent results across browsers. Fix: Use `parse()` with explicit format for non-ISO date strings: `parse('02/23/2026', 'MM/dd/yyyy', new Date())`. [src3]
- **Day.js diff() uses singular units**: Moment uses `diff(other, 'days')` (plural), Day.js uses `diff(other, 'day')` (singular). Day.js accepts both, but TypeScript types may complain with plurals. Fix: Use singular unit names: `'day'`, `'month'`, `'year'`. [src4]

## Diagnostic Commands

```bash
# 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

| Library | Version | Status | Key Features | Node.js |
|---|---|---|---|---|
| Moment.js 2.x | Maintenance only | Legacy — no new features, no v3 planned | Mutable API, bundled locales, moment-timezone | Any |
| date-fns 4.3.0 | Current (latest v4.x) | Active | First-class TZ via @date-fns/tz, ESM-first, `format`/`formatISO`/`formatRFC3339` TZ support, 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), Safari partial | Native date/time, immutable, timezone-aware, no dependencies | 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 is important for client-side performance | Need a complete ISO 8601 / RFC 2822 parser | Luxon or Temporal (future) |
| Want functional programming style (choose date-fns) | Team prefers chainable object API | Choose 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 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 on Moment.
- date-fns v4 has breaking changes in timezone handling compared to v3's `date-fns-tz` package. If upgrading from v3, review the @date-fns/tz migration guide separately. The v4.1 release added timezone support to `format`, `formatISO`, `formatISO9075`, `formatRelative`, and `formatRFC3339`.
- 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 the `@js-temporal/polyfill` package. date-fns has an "Interim API" experiment underway to offer a Temporal-compatible build alongside its `Date` API. [src8, src9]
- Both date-fns and Day.js use native JavaScript Date objects internally, which means they inherit Date's UTC vs local time gotchas. Be explicit about timezone expectations.
- The `@js-temporal/polyfill` adds ~40 KB to your bundle — comparable to Moment.js itself. Wait for native browser support to stabilize before adopting Temporal for bundle-size-sensitive applications.

## Related Units

- [JavaScript to TypeScript Migration](/software/migrations/javascript-to-typescript/2026)
- [Webpack to Vite Migration](/software/migrations/webpack-to-vite/2026)
- [jQuery to React Migration](/software/migrations/jquery-to-react/2026)
