Skip to content

Fix: date-fns Not Working — Wrong Timezone Output, Invalid Date, or Locale Not Applied

FixDevs ·

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 UTC

Or 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 Date

Or 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 operates on JavaScript Date objects, which are always in the local system timezone:

  • format() uses local timezone by default — a Date object 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 need date-fns-tz.
  • parse() requires a reference date for context — the third argument to parse() provides default values for any time components not in the format string. Passing new Date() is standard, but passing new 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. Use import syntax or check your bundler’s ESM configuration.

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-tz
import { 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 object

Format 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 DST

Fix 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:30

Common 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 v3

If 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 ESM

Fix 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 clientnew 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.

For related date handling, see Fix: JavaScript Date Not Working and Fix: Python datetime Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles