Fix: date-fns Not Working — Wrong Timezone Output, Invalid Date, or Locale Not Applied
Part of: React & Frontend Errors
Quick Answer
How to fix date-fns issues — timezone handling with date-fns-tz, parseISO vs new Date, locale import and configuration, DST edge cases, v3 ESM migration, and common format pattern mistakes.
The Problem
format() shows the wrong time due to timezone issues:
import { format } from 'date-fns';
const date = new Date('2024-03-15T10:00:00Z');
format(date, 'HH:mm'); // Shows '11:00' — wrong, expected '10:00'
// Displays in local timezone, not UTCOr parse() returns an Invalid Date:
import { parse } from 'date-fns';
const date = parse('2024-03-15', 'yyyy-MM-dd', new Date());
console.log(isValid(date)); // false — Invalid DateOr a locale isn’t applied despite being passed:
import { format } from 'date-fns';
import { ja } from 'date-fns/locale';
format(new Date(), 'MMMM', { locale: ja }); // Returns 'March' not '3月'Or after upgrading to date-fns v3, imports break:
SyntaxError: The requested module 'date-fns' does not provide an export named 'parse'Why This Happens
date-fns is intentionally a thin wrapper around the native Date object. It does not introduce a new immutable type like Luxon’s DateTime or Day.js’s wrapped instance. Every function takes a Date in, returns a Date (or string/number) out, and inherits every quirk of the underlying object. That includes the fact that a Date only stores an instant in time — a Unix timestamp — and the timezone you see is whatever timezone your runtime was launched in. Server Node processes default to UTC. Browsers use the OS timezone. Vercel functions and Cloudflare Workers can be either depending on region. This is the root cause of “works locally, wrong time in production.”
The locale story trips up almost every project on first encounter. Unlike Intl.DateTimeFormat, which ships with every locale baked into the runtime, date-fns ships zero locales by default. format(date, 'MMMM', { locale: ja }) only works if you import { ja } from 'date-fns/locale' first. The package is designed this way so that tree-shaking bundlers can drop unused locales — but if you forget the import, you do not get an error, you get English. Some Vite and webpack configurations also fail to tree-shake the locale barrel correctly and pull all 100+ locales into the bundle, ballooning the build by megabytes.
The v3 ESM-only break is the third major class of failure. v3 dropped CommonJS, which means Jest with the default babel-jest transformer, older Node services using require(), and any bundler that has not been configured for ESM will crash on import. The fix is rarely “downgrade to v2” — it is to fix the transformer config, but the error message rarely points there.
format()uses local timezone by default — aDateobject stores an instant in time (Unix timestamp).format()converts it to a string using the local timezone. If you want UTC or a specific timezone, you needdate-fns-tz.parse()requires a reference date for context — the third argument toparse()provides default values for any time components not in the format string. Passingnew Date()is standard, but passingnew Date(0)(epoch) can cause unexpected results if the format string doesn’t include all components.- Locale must be explicitly imported — date-fns doesn’t bundle locales. Each locale is a separate import. The locale must be passed as an option to functions that use it (
format,formatDistance,formatRelative). - v3 changed to pure ESM — date-fns v3 dropped CommonJS support. Importing with
require()or using CJS module resolution fails. Useimportsyntax or check your bundler’s ESM configuration. - Tree-shake leakage —
import { format } from 'date-fns'is supposed to drop other functions, butimport * as fns from 'date-fns'or barrel re-exports inside your own code defeat it. The bundle suddenly weighs 80kB instead of 6kB.
Diagnostic Timeline
The first instinct when date-fns “doesn’t work” is to swap new Date(str) for parseISO(str) and hope. That fixes maybe a third of cases. The real root cause is usually somewhere else — work the layers in order.
Minute 0 — observe. Print the date in three forms: date.toISOString(), date.toString(), and format(date, 'yyyy-MM-dd HH:mm:ssXXX'). If toISOString() is what you expected (UTC) but format() is wrong, you have a timezone display bug, not a parsing bug. If toISOString() itself is wrong, the input string parsed incorrectly upstream.
Minute 2 — confirm the runtime timezone. Run Intl.DateTimeFormat().resolvedOptions().timeZone in both your dev console and on the server. Production Node defaulting to UTC while your dev machine sits in Asia/Tokyo accounts for nine out of ten “off by N hours” reports. The fix is formatInTimeZone from date-fns-tz, not “set TZ env var.”
Minute 5 — check the locale import. If format(date, 'MMMM', { locale: ja }) returns “March,” your import { ja } from 'date-fns/locale' either failed silently (typo in identifier — ja vs jaJP) or the v3 path moved. Log the imported ja object — if it is undefined, the import is wrong. v3 consolidated locale paths to date-fns/locale from date-fns/locale/ja, and stale lockfiles can keep the wrong path resolving to nothing.
Minute 8 — verify the bundler ESM mode. If the import works in dev but fails at build time with “does not provide an export named X,” your bundler is treating date-fns as CJS. In Jest add transformIgnorePatterns: ['/node_modules/(?!date-fns)/']. In Next.js older than 14, set transpilePackages: ['date-fns'] in next.config.js. In Vite, this normally works out of the box — if it does not, check optimizeDeps.include.
Minute 12 — inspect bundle weight. Run npx source-map-explorer or npx vite-bundle-visualizer and look for the date-fns directory size. If it is over 30kB, tree-shake leaked. Search your code for import * as from date-fns, dynamic property access (fns[funcName]), or barrel files that re-export it. Replace each with named imports.
Fix 1: Handle Timezones Correctly
date-fns core works in local time. For UTC and named timezones, use date-fns-tz:
npm install date-fns-tzimport { format } from 'date-fns';
import { formatInTimeZone, toZonedTime, fromZonedTime } from 'date-fns-tz';
const utcDate = new Date('2024-03-15T10:00:00Z');
// Format in local timezone (default behavior)
format(utcDate, 'HH:mm'); // '11:00' if local is UTC+1
// Format in a specific timezone
formatInTimeZone(utcDate, 'America/New_York', 'HH:mm'); // '06:00'
formatInTimeZone(utcDate, 'Asia/Tokyo', 'HH:mm yyyy-MM-dd'); // '19:00 2024-03-15'
formatInTimeZone(utcDate, 'UTC', 'HH:mm'); // '10:00'
// Convert UTC to a specific timezone (for display)
const tokyoDate = toZonedTime(utcDate, 'Asia/Tokyo');
format(tokyoDate, 'HH:mm'); // '19:00'
// Convert from a timezone to UTC (for storage)
const localDateInTokyo = new Date('2024-03-15T19:00:00');
const utcEquivalent = fromZonedTime(localDateInTokyo, 'Asia/Tokyo');
// Returns the UTC Date objectFormat UTC timestamps correctly:
import { formatInTimeZone } from 'date-fns-tz';
// User sees dates in their preferred timezone
function formatForUser(isoString: string, userTimezone: string): string {
return formatInTimeZone(
new Date(isoString),
userTimezone,
'MMM d, yyyy HH:mm'
);
}
// Store user's timezone
const userTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
// 'America/New_York', 'Europe/London', etc.DST edge cases:
import { addDays, addHours } from 'date-fns';
import { formatInTimeZone, toZonedTime } from 'date-fns-tz';
// DST transition: clocks spring forward 2024-03-10 at 2am in US/Eastern
const beforeDST = new Date('2024-03-10T06:00:00Z'); // 1am Eastern
const afterDST = addHours(beforeDST, 2); // 3am Eastern (DST starts)
formatInTimeZone(beforeDST, 'America/New_York', 'HH:mm'); // '01:00'
formatInTimeZone(afterDST, 'America/New_York', 'HH:mm'); // '03:00' (skips 2am)
// Adding days across DST — use date-fns addDays (timezone-aware)
const april1 = new Date('2024-04-01T00:00:00');
const april2 = addDays(april1, 1); // Correctly handles DSTFix 2: Fix parse() and Date Parsing
import { parse, parseISO, isValid } from 'date-fns';
// parseISO — for ISO 8601 strings (YYYY-MM-DD or full datetime)
const date1 = parseISO('2024-03-15'); // Date in local timezone
const date2 = parseISO('2024-03-15T10:00:00Z'); // UTC datetime
const date3 = parseISO('2024-03-15T10:00:00+09:00'); // With offset
// parse() — for custom date formats
const date4 = parse('15/03/2024', 'dd/MM/yyyy', new Date());
const date5 = parse('March 15, 2024', 'MMMM d, yyyy', new Date());
const date6 = parse('2024-03-15 10:30', 'yyyy-MM-dd HH:mm', new Date());
// Always validate after parsing
if (!isValid(date4)) {
throw new Error('Invalid date string');
}
// WRONG — new Date() with non-ISO strings is unreliable
const bad = new Date('15/03/2024'); // Invalid in most browsers
const bad2 = new Date('March 15 2024'); // May vary by engine
// WRONG — parse with wrong reference date causes off-by-one
const withEpoch = parse('10:30', 'HH:mm', new Date(0));
// Month is January 1970! Avoid new Date(0) as reference
// CORRECT — use new Date() as reference
const time = parse('10:30', 'HH:mm', new Date());
// Date is today with time 10:30Common format patterns:
import { format } from 'date-fns';
const date = new Date('2024-03-15T10:30:45Z');
format(date, 'yyyy-MM-dd'); // '2024-03-15'
format(date, 'dd/MM/yyyy'); // '15/03/2024'
format(date, 'MM/dd/yyyy'); // '03/15/2024'
format(date, 'MMMM d, yyyy'); // 'March 15, 2024'
format(date, 'MMM d'); // 'Mar 15'
format(date, 'HH:mm'); // '10:30' (24h)
format(date, 'hh:mm a'); // '10:30 AM' (12h)
format(date, 'yyyy-MM-dd HH:mm:ss'); // '2024-03-15 10:30:45'
format(date, "yyyy-MM-dd'T'HH:mm:ssxxx"); // '2024-03-15T10:30:45+00:00'
// PITFALL — 'YYYY' is week-year, not calendar year
format(new Date('2024-12-31'), 'YYYY-MM-dd'); // '2025-12-31'! (week year)
format(new Date('2024-12-31'), 'yyyy-MM-dd'); // '2024-12-31' ✓
// PITFALL — 'DD' is day of year, not day of month
format(new Date('2024-03-15'), 'DD'); // '75' (75th day of 2024)
format(new Date('2024-03-15'), 'dd'); // '15' ✓Fix 3: Apply Locales Correctly
import { format, formatDistance, formatRelative } from 'date-fns';
import { ja, fr, de, es, zhCN, ko } from 'date-fns/locale';
const date = new Date('2024-03-15');
const now = new Date();
// Format with locale
format(date, 'MMMM d, yyyy', { locale: ja }); // '3月 15, 2024'
format(date, 'MMMM d, yyyy', { locale: fr }); // 'mars 15, 2024'
format(date, 'PPP', { locale: de }); // '15. März 2024' (localized date)
// Relative time
formatDistance(date, now, { locale: ja, addSuffix: true }); // '約7ヶ月前'
formatDistance(date, now, { locale: fr, addSuffix: true }); // 'il y a environ 7 mois'
// formatRelative
formatRelative(subDays(new Date(), 1), new Date(), { locale: fr }); // 'hier à ...'
// Full date representation with locale
format(date, 'P', { locale: de }); // '15.03.2024' (locale-specific short date)
format(date, 'PP', { locale: de }); // '15. März 2024'
format(date, 'PPP', { locale: de }); // '15. März 2024'
format(date, 'PPPP', { locale: de }); // 'Freitag, 15. März 2024'Set a default locale globally (v3):
// date-fns v3 — use setDefaultOptions
import { setDefaultOptions } from 'date-fns';
import { ja } from 'date-fns/locale';
setDefaultOptions({ locale: ja });
// Now all functions use Japanese locale by default
format(new Date(), 'MMMM'); // '3月'
formatDistance(pastDate, new Date(), { addSuffix: true }); // '...前'
// Override per-call
format(new Date(), 'MMMM', { locale: fr }); // 'mars'Fix 4: Common date-fns Operations
import {
addDays, addMonths, addYears, addHours, addMinutes,
subDays, subMonths, subWeeks,
startOfDay, endOfDay, startOfMonth, endOfMonth, startOfWeek,
isBefore, isAfter, isEqual, isWithinInterval,
differenceInDays, differenceInHours, differenceInMonths,
getDay, getMonth, getYear, getHours,
setHours, setMinutes,
eachDayOfInterval,
} from 'date-fns';
const now = new Date();
// Arithmetic
const tomorrow = addDays(now, 1);
const lastMonth = subMonths(now, 1);
const nextYear = addYears(now, 1);
const in3Hours = addHours(now, 3);
// Boundaries
const todayStart = startOfDay(now); // 2024-03-15T00:00:00
const todayEnd = endOfDay(now); // 2024-03-15T23:59:59.999
const monthStart = startOfMonth(now);
const monthEnd = endOfMonth(now);
const weekStart = startOfWeek(now, { weekStartsOn: 1 }); // Monday
// Comparisons
isBefore(yesterday, now); // true
isAfter(tomorrow, now); // true
isEqual(now, new Date(now.getTime())); // true
// Check if date is in a range
isWithinInterval(now, { start: monthStart, end: monthEnd }); // true
// Differences
differenceInDays(tomorrow, now); // 1
differenceInHours(tomorrow, now); // 24
differenceInMonths(nextYear, now); // 12
// Get all dates in a range (for calendars)
const daysInRange = eachDayOfInterval({
start: monthStart,
end: monthEnd,
});
// Modify time components
const meetingAt2pm = setHours(setMinutes(now, 0), 14);Fix 5: Migrate to date-fns v3
date-fns v3 is pure ESM — CommonJS imports break:
// v2 — CJS require (still works in v2, breaks in v3 without bundler support)
const { format } = require('date-fns');
// v3 — ESM import only
import { format } from 'date-fns';
// v3 changes:
// 1. Locale imports changed path
// v2:
import { ja } from 'date-fns/locale/ja'; // Old path
// v3:
import { ja } from 'date-fns/locale'; // New path — all locales from one export
// OR individual import:
import { ja } from 'date-fns/locale/ja'; // Still works in v3
// 2. No more default export
// v2:
import dateFns from 'date-fns'; // Doesn't work in v2 either, but some did this
// v3: Always use named imports
// 3. TypeScript types are included (no @types/date-fns needed)
// 4. setDefaultOptions added for global locale
import { setDefaultOptions } from 'date-fns'; // New in v3If you can’t upgrade to ESM, use the compatibility export:
// For CJS environments in v3
const { format } = require('date-fns'); // May work if bundler handles ESM→CJS
// If not, use v2 or upgrade to ESMFix 6: Format Relative Time Correctly
import { formatDistance, formatDistanceToNow, formatDistanceStrict, intlFormatDistance } from 'date-fns';
const now = new Date();
const fiveMinutesAgo = subMinutes(now, 5);
const twoHoursAgo = subHours(now, 2);
const yesterday = subDays(now, 1);
// Fuzzy relative time
formatDistanceToNow(fiveMinutesAgo); // '5 minutes'
formatDistanceToNow(fiveMinutesAgo, { addSuffix: true }); // '5 minutes ago'
formatDistanceToNow(twoHoursAgo, { addSuffix: true }); // 'about 2 hours ago'
// Strict (no rounding)
formatDistanceStrict(fiveMinutesAgo, now); // '5 minutes'
formatDistanceStrict(twoHoursAgo, now, { unit: 'minute' }); // '120 minutes'
// Native Intl API wrapper (v3+)
intlFormatDistance(fiveMinutesAgo, now); // '5 minutes ago'
intlFormatDistance(yesterday, now, { locale: 'ja' }); // '1日前'
// Supports: 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year'
// Custom formatting for display
function timeAgo(date: Date): string {
const now = new Date();
const diffInSeconds = differenceInSeconds(now, date);
if (diffInSeconds < 60) return 'just now';
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
return format(date, 'MMM d'); // Show date for older items
}Still Not Working?
format() throws “RangeError: Invalid time value” — the Date object is invalid. Check the input: isValid(date) returns false for invalid dates. Common causes: parsing a non-ISO string with new Date(), passing undefined, or a date arithmetic operation that produces overflow. Always validate before formatting.
Inconsistent results between server and client — new Date() uses the system timezone. On a server running in UTC, new Date('2024-03-15') is midnight UTC. In a browser with a local timezone, the same string may be a different day entirely. Use parseISO() for strings and formatInTimeZone() for display to get consistent behavior regardless of where the code runs.
Bundle size concerns — date-fns is tree-shakeable. Import only the functions you use: import { format } from 'date-fns' (not import * as dateFns from 'date-fns'). If using webpack or Rollup, they’ll eliminate unused exports. date-fns v3 modular imports reduce bundle size compared to v2.
parseISO returns Invalid Date for SQL timestamps — parseISO expects strict ISO 8601. PostgreSQL’s 2024-03-15 10:00:00+00 (space instead of T, no leading zero on offset) fails. Either replace the space with T before parsing, or use the parse(str, "yyyy-MM-dd HH:mm:ssXX", new Date()) form with an explicit format string. SQLite and MySQL DATETIME columns have the same problem.
Jest crashes with “SyntaxError: Cannot use import statement outside a module” — v3 ESM hits Jest’s default CommonJS pipeline. Add transformIgnorePatterns: ['node_modules/(?!(date-fns|date-fns-tz)/)'] to jest.config.js. If using ts-jest, set useESM: true in the preset and update tsconfig.json to "module": "esnext". Vitest does not have this problem.
differenceInDays returns 0 across DST boundary — differenceInDays rounds down based on calendar days, but differenceInHours between 1am and 1am next day across a DST transition is 23 or 25 hours, not 24. If you need exact day boundaries, use differenceInCalendarDays instead — it counts midnight-to-midnight transitions regardless of clock changes.
For related date handling, see Fix: Zod Validation Not Working, Fix: Valibot Not Working, Fix: MySQL Error 1064 Syntax Error, and Fix: TanStack Query Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: pdf-lib Not Working — PDF Not Generating, Fonts Not Embedding, or Pages Blank
How to fix pdf-lib issues — creating PDFs from scratch, modifying existing PDFs, embedding fonts and images, form filling, merging documents, and browser and Node.js usage.
Fix: Pusher Not Working — Events Not Received, Channel Auth Failing, or Connection Dropping
How to fix Pusher real-time issues — client and server setup, channel types, presence channels, authentication endpoints, event binding, connection management, and React integration.
Fix: Zod Validation Not Working — safeParse Returns Wrong Error, transform Breaks Type, or discriminatedUnion Fails
How to fix Zod schema validation issues — parse vs safeParse, transform and preprocess, refine for cross-field validation, discriminatedUnion, error formatting, and common schema mistakes.
Fix: TypeScript Function Overload Error — No Overload Matches This Call
How to fix TypeScript function overload errors — overload signature compatibility, implementation signature, conditional types as alternatives, method overloads in classes, and common pitfalls.