JavaScript Intl.DateTimeFormat Complete Guide

Every option, every timezone, every pitfall. With a live playground you can test right here.

By Michael Lip · May 16, 2026 · 18 min read

What Is Intl.DateTimeFormat?

Intl.DateTimeFormat is a built-in JavaScript constructor for creating locale-sensitive date and time formatters. It is part of the ECMAScript Internationalization API (ECMA-402) and is available in every modern browser and Node.js without any library.

At its core, the API solves three problems that previously required external libraries like Moment.js or date-fns:

  1. Locale-aware formatting — dates appear in the format expected by the reader (month/day/year in the US, day/month/year in Europe, year-month-day in Japan).
  2. Timezone conversion — the formatter reads the full IANA timezone database compiled into the JavaScript engine, so DST transitions, historical offset changes, and half-hour/quarter-hour timezones like Asia/Kolkata (UTC+5:30) are handled correctly.
  3. Consistent output — the same options object produces the same structure on every platform, because the behavior is specified in the ECMA-402 standard and backed by ICU (International Components for Unicode) data.

The constructor signature is:

const formatter = new Intl.DateTimeFormat(locales, options);
const result   = formatter.format(date);

locales is a BCP 47 language tag string (like "en-US", "ja-JP", or "ar-EG") or an array of tags in priority order. options is an object that controls which date/time components appear, how they appear, and which timezone to use. Both arguments are optional — if omitted, the formatter uses the runtime's default locale and timezone.

You can also call the static shorthand Intl.DateTimeFormat().format(date) for a quick one-off, but creating a named instance and reusing it is significantly faster when formatting multiple dates (see Performance below).

Unlike Date.prototype.toLocaleString(), which creates and discards a new formatter on every call, Intl.DateTimeFormat lets you create a single formatter instance and reuse it across thousands of .format() calls. It also gives you formatToParts() for granular control and formatRange() for date spans — neither of which are available through toLocaleString.

Browser Support

The base Intl.DateTimeFormat API shipped years ago. Newer features like dateStyle/timeStyle and formatRange were added incrementally. Here is the current compatibility matrix:

Feature Chrome Firefox Safari Edge Node.js
Core constructor 24+ 29+ 10+ 12+ 0.12+
timeZone (IANA names) 24+ 52+ 10+ 14+ 4+
dateStyle / timeStyle 76+ 79+ 14.1+ 79+ 12.9+
formatToParts() 57+ 51+ 11+ 18+ 8+
formatRange() 76+ 91+ 14.1+ 79+ 12.9+
fractionalSecondDigits 84+ 84+ 14.1+ 84+ 14.1+
dayPeriod 92+ 90+ 15+ 92+ 16.6+
hourCycle 73+ 79+ 13+ 18+ 12+

In short: if your audience uses any browser released after 2021, every feature in this guide works. For Node.js, version 14+ covers everything. The only environment that requires polyfills is IE11, which is end-of-life and should not be targeted in new projects.

Live Intl.DateTimeFormat Playground

Configure every option below and see the formatted output in real time. The generated code snippet is ready to copy into your project.

Loading...

        
      

Basic Usage

The simplest call uses no options at all. The formatter falls back to the user's operating system locale and timezone:

// Uses the runtime's default locale and timezone
new Intl.DateTimeFormat().format(new Date());
// e.g. "5/16/2026" on a US-English system

To control the output, pass a locale and options:

const date = new Date('2026-05-16T10:30:00Z');

// US English, long date + short time
new Intl.DateTimeFormat('en-US', {
  dateStyle: 'long',
  timeStyle: 'short',
  timeZone: 'America/New_York'
}).format(date);
// "May 16, 2026 at 6:30 AM"

// German, full date in Berlin timezone
new Intl.DateTimeFormat('de-DE', {
  dateStyle: 'full',
  timeStyle: 'long',
  timeZone: 'Europe/Berlin'
}).format(date);
// "Samstag, 16. Mai 2026 um 12:30:00 MESZ"

// Indian English, IST timezone
new Intl.DateTimeFormat('en-IN', {
  dateStyle: 'full',
  timeStyle: 'long',
  timeZone: 'Asia/Kolkata'
}).format(date);
// "Saturday, 16 May 2026 at 4:00:00 pm IST"

Notice the three-part pattern: locale controls the language and formatting conventions, options control which components appear and how they appear, and timeZone controls which moment of the day is shown. The underlying Date object is always the same UTC instant.

The static shorthand new Date().toLocaleString(locale, options) uses the same engine internally but creates and discards a new formatter each time. When you format more than a few dates, constructing a reusable Intl.DateTimeFormat instance is the better approach.

Complete Options Reference

Every property you can pass in the options object, with values and example output:

OptionValuesExample Output (en-US)Notes
dateStyle "full", "long", "medium", "short" Saturday, May 16, 2026 / May 16, 2026 / May 16, 2026 / 5/16/26 Cannot combine with individual date/time component options
timeStyle "full", "long", "medium", "short" 10:30:00 AM Eastern Daylight Time / 10:30:00 AM EDT / 10:30:00 AM / 10:30 AM Cannot combine with individual date/time component options
year "numeric", "2-digit" 2026 / 26
month "numeric", "2-digit", "long", "short", "narrow" 5 / 05 / May / May / M "narrow" can be ambiguous (J = January, June, July)
day "numeric", "2-digit" 16 / 16 2-digit pads single digits: 5 becomes 05
weekday "long", "short", "narrow" Saturday / Sat / S
hour "numeric", "2-digit" 10 / 10 Automatically adds AM/PM for 12-hour locales
minute "numeric", "2-digit" 30 / 30
second "numeric", "2-digit" 0 / 00
fractionalSecondDigits 1, 2, 3 0.1 / 0.12 / 0.123 Millisecond precision. Requires second to be set.
timeZone IANA name or "UTC" n/a (changes the output time) Full list: EpochPilot timezone tool
timeZoneName "short", "long", "shortOffset", "longOffset", "shortGeneric", "longGeneric" EST / Eastern Standard Time / GMT-5 / GMT-05:00 / ET / Eastern Time shortOffset and longOffset added in 2021
hourCycle "h11", "h12", "h23", "h24" 0-11 / 1-12 / 0-23 / 1-24 Overrides locale default. Use h23 for 24-hour time.
hour12 true, false 12-hour / 24-hour Legacy; prefer hourCycle
era "long", "short", "narrow" Anno Domini / AD / A Useful for historical dates or non-Gregorian calendars
dayPeriod "narrow", "short", "long" in the morning / AM / in the morning Locale-dependent; not all locales distinguish periods beyond AM/PM
calendar "gregory", "islamic", "persian", "hebrew", etc. Varies by calendar system Defaults to locale's preferred calendar
numberingSystem "latn", "arab", "deva", "thai", etc. 1234 / ١٢٣٤ / etc. Controls digit characters used

dateStyle and timeStyle Shorthand

The dateStyle and timeStyle properties are convenience presets added in ES2020. They map to a predefined set of individual component options, chosen by the locale. The available values are "full", "long", "medium", and "short".

// These two produce the same output in en-US:
new Intl.DateTimeFormat('en-US', { dateStyle: 'long' }).format(date);
// "May 16, 2026"

new Intl.DateTimeFormat('en-US', {
  year: 'numeric', month: 'long', day: 'numeric'
}).format(date);
// "May 16, 2026"

The critical rule: you cannot mix dateStyle/timeStyle with individual component options. This throws a TypeError:

// WRONG: TypeError!
new Intl.DateTimeFormat('en-US', {
  dateStyle: 'long',
  hour: 'numeric'  // Cannot mix with dateStyle
});

You can use dateStyle and timeStyle together, and you can combine them with timeZone, hourCycle, calendar, and numberingSystem — those are structural options, not component options.

Here is what each dateStyle level looks like across four locales:

dateStyleen-USde-DEja-JPar-SA
full Saturday, May 16, 2026 Samstag, 16. Mai 2026 2026年5月16日土曜日 السبت، ١٦ مايو ٢٠٢٦
long May 16, 2026 16. Mai 2026 2026年5月16日 ١٦ مايو ٢٠٢٦
medium May 16, 2026 16.05.2026 2026/05/16 ١٦/٠٥/٢٠٢٦
short 5/16/26 16.05.26 2026/05/16 ١٦/٥/٢٦

When choosing between the style shorthand and individual components: use dateStyle/timeStyle when the locale's default layout is acceptable. Use individual components when you need specific control — for example, showing month and year without the day, or including the weekday but omitting the year.

Timezone Handling

The timeZone option accepts any valid IANA timezone identifier. The JavaScript engine's built-in timezone database contains hundreds of entries covering every region, including historical changes and DST rules. You can list all supported timezones with Intl.supportedValuesOf('timeZone') (Chrome 93+, Node.js 18+).

UTC

new Intl.DateTimeFormat('en-US', {
  timeZone: 'UTC',
  dateStyle: 'medium',
  timeStyle: 'long'
}).format(new Date());
// "May 16, 2026 at 2:30:00 PM UTC"

Named Timezones

const now = new Date();

// Asia/Kolkata (Indian Standard Time, UTC+5:30)
new Intl.DateTimeFormat('en-IN', {
  timeZone: 'Asia/Kolkata',
  dateStyle: 'full',
  timeStyle: 'long'
}).format(now);
// "Saturday, 16 May 2026 at 8:00:00 pm IST"

// America/Los_Angeles (Pacific Time)
new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/Los_Angeles',
  dateStyle: 'full',
  timeStyle: 'long'
}).format(now);
// "Saturday, May 16, 2026 at 7:30:00 AM PDT"

// Pacific/Auckland (New Zealand)
new Intl.DateTimeFormat('en-NZ', {
  timeZone: 'Pacific/Auckland',
  dateStyle: 'full',
  timeStyle: 'long'
}).format(now);
// "Sunday, 17 May 2026 at 2:30:00 am NZST"

DST Transitions

Intl.DateTimeFormat handles Daylight Saving Time automatically. The same timezone identifier produces different abbreviations and offsets depending on the date:

const fmt = (d) => new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/New_York',
  timeStyle: 'long'
}).format(d);

fmt(new Date('2026-01-15T12:00:00Z'));
// "7:00:00 AM EST"  (standard time, UTC-5)

fmt(new Date('2026-07-15T12:00:00Z'));
// "8:00:00 AM EDT"  (daylight time, UTC-4)

You never need to calculate offsets or detect DST yourself. The engine handles it. This is one of the primary reasons to use Intl.DateTimeFormat over manual offset arithmetic.

IST to UTC Conversion

A common search: "10pm IST to UTC." You format the same instant with two different timezone settings:

// Create a date that represents 10:00 PM IST on May 16, 2026
// IST = UTC+5:30, so 10:00 PM IST = 4:30 PM UTC
const istDate = new Date('2026-05-16T16:30:00Z');

const istFmt = new Intl.DateTimeFormat('en-IN', {
  timeZone: 'Asia/Kolkata',
  dateStyle: 'medium',
  timeStyle: 'short'
});

const utcFmt = new Intl.DateTimeFormat('en-US', {
  timeZone: 'UTC',
  dateStyle: 'medium',
  timeStyle: 'short'
});

console.log('IST:', istFmt.format(istDate));
// "IST: 16-May-2026, 10:00 pm"

console.log('UTC:', utcFmt.format(istDate));
// "UTC: May 16, 2026, 4:30 PM"

For a visual timezone converter, use EpochPilot's main timezone tool.

Half-Hour and Quarter-Hour Offsets

Several real-world timezones use offsets that are not whole hours. Intl.DateTimeFormat handles all of them correctly:

// UTC+5:30 — India (Asia/Kolkata)
// UTC+5:45 — Nepal (Asia/Kathmandu)
// UTC+9:30 — Australia Central (Australia/Adelaide, with DST)
// UTC+8:45 — Australia Western (Australia/Eucla)
// UTC+6:30 — Myanmar (Asia/Yangon)
// UTC+3:30 — Iran (Asia/Tehran, with DST)

const zones = [
  'Asia/Kolkata', 'Asia/Kathmandu', 'Australia/Adelaide',
  'Asia/Yangon', 'Asia/Tehran'
];

zones.forEach(tz => {
  const offset = new Intl.DateTimeFormat('en-US', {
    timeZone: tz, timeZoneName: 'longOffset'
  }).formatToParts(new Date())
    .find(p => p.type === 'timeZoneName')?.value;
  console.log(`${tz}: ${offset}`);
});
// Asia/Kolkata: GMT+05:30
// Asia/Kathmandu: GMT+05:45
// Australia/Adelaide: GMT+09:30 (or +10:30 during DST)
// Asia/Yangon: GMT+06:30
// Asia/Tehran: GMT+03:30 (or +04:30 during DST)

The Locale System

The first argument to Intl.DateTimeFormat is a BCP 47 language tag. This tag has a structured format:

language[-script][-region][-extension]
// Examples:
"en"       // English (any region)
"en-US"    // English as used in the United States
"en-GB"    // English as used in the United Kingdom
"zh-Hans"  // Chinese, Simplified script
"ar-EG"    // Arabic as used in Egypt
"ja-JP-u-ca-japanese"  // Japanese with Japanese calendar

The -u Unicode extension lets you embed options directly in the locale tag. This can be more convenient when passing locale strings through APIs that do not accept separate options objects:

// These are equivalent:
new Intl.DateTimeFormat('en-US-u-hc-h23');
new Intl.DateTimeFormat('en-US', { hourCycle: 'h23' });

// Calendar via locale tag:
new Intl.DateTimeFormat('ar-SA-u-ca-islamic');
// Same as:
new Intl.DateTimeFormat('ar-SA', { calendar: 'islamic' });

// Numbering system via locale tag:
new Intl.DateTimeFormat('hi-IN-u-nu-deva');
// Same as:
new Intl.DateTimeFormat('hi-IN', { numberingSystem: 'deva' });

You can pass an array of locales as a fallback chain. The engine picks the first one it fully supports:

new Intl.DateTimeFormat(['zh-Hant-TW', 'zh-TW', 'zh', 'en-US'], {
  dateStyle: 'long'
}).format(date);

To check which locale was actually selected and what the resolved options are, call resolvedOptions():

const fmt = new Intl.DateTimeFormat('en-IN', {
  timeZone: 'Asia/Kolkata',
  dateStyle: 'full',
  timeStyle: 'long'
});

console.log(fmt.resolvedOptions());
// {
//   locale: "en-IN",
//   calendar: "gregory",
//   numberingSystem: "latn",
//   timeZone: "Asia/Calcutta",  // canonical name
//   dateStyle: "full",
//   timeStyle: "long",
//   ...
// }

Note: some engines return the canonical (older) timezone name like "Asia/Calcutta" even when you pass "Asia/Kolkata". Both refer to the same timezone and produce identical output.

formatToParts() Deep Dive

While format() returns a single string, formatToParts() returns an array of typed tokens. This is essential when you need to style, rearrange, or extract specific components:

const fmt = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/New_York',
  weekday: 'long',
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  hour: 'numeric',
  minute: '2-digit',
  timeZoneName: 'short'
});

const parts = fmt.formatToParts(new Date('2026-05-16T14:30:00Z'));
console.log(parts);
// [
//   { type: "weekday",      value: "Saturday" },
//   { type: "literal",      value: ", " },
//   { type: "month",        value: "May" },
//   { type: "literal",      value: " " },
//   { type: "day",          value: "16" },
//   { type: "literal",      value: ", " },
//   { type: "year",         value: "2026" },
//   { type: "literal",      value: ", " },
//   { type: "hour",         value: "10" },
//   { type: "literal",      value: ":" },
//   { type: "minute",       value: "30" },
//   { type: "literal",      value: " " },
//   { type: "dayPeriod",    value: "AM" },
//   { type: "literal",      value: " " },
//   { type: "timeZoneName", value: "EDT" }
// ]

Each object has a type (the component name) and a value (the formatted string for that component). Literal parts contain the separator characters (commas, spaces, colons) that the locale inserts between components.

Practical Use: Custom Styled Output

// Wrap each part in a span with its type as a CSS class
function styledDate(date, locale, options) {
  const parts = new Intl.DateTimeFormat(locale, options)
    .formatToParts(date);
  return parts.map(p =>
    p.type === 'literal'
      ? p.value
      : `<span class="dt-${p.type}">${p.value}</span>`
  ).join('');
}

// Result: <span class="dt-month">May</span> <span class="dt-day">16</span>...
// Now style .dt-month { color: blue; } .dt-day { font-weight: bold; }

Practical Use: Extract a Single Part

function getTimezoneName(date, tz) {
  return new Intl.DateTimeFormat('en-US', {
    timeZone: tz,
    timeZoneName: 'long'
  }).formatToParts(date)
    .find(p => p.type === 'timeZoneName')?.value;
}

getTimezoneName(new Date(), 'Asia/Kolkata');
// "India Standard Time"

getTimezoneName(new Date('2026-07-01'), 'America/New_York');
// "Eastern Daylight Time"

Practical Use: Build an Object from Parts

function dateParts(date, tz) {
  const parts = new Intl.DateTimeFormat('en-US', {
    timeZone: tz,
    year: 'numeric', month: '2-digit', day: '2-digit',
    hour: '2-digit', minute: '2-digit', second: '2-digit',
    hourCycle: 'h23'
  }).formatToParts(date);

  const obj = {};
  parts.forEach(p => { if (p.type !== 'literal') obj[p.type] = p.value; });
  return obj;
}

dateParts(new Date(), 'Asia/Kolkata');
// { month: "05", day: "16", year: "2026", hour: "20", minute: "00", second: "00" }

formatRange() for Date Spans

formatRange() formats two dates as a human-readable range, automatically collapsing shared components when the dates fall in the same month or year:

const fmt = new Intl.DateTimeFormat('en-US', {
  year: 'numeric',
  month: 'long',
  day: 'numeric'
});

const start = new Date('2026-05-16');
const end   = new Date('2026-05-22');

fmt.formatRange(start, end);
// "May 16 – 22, 2026"  (shared month + year)

const end2 = new Date('2026-06-03');
fmt.formatRange(start, end2);
// "May 16 – June 3, 2026"  (shared year)

const end3 = new Date('2027-01-10');
fmt.formatRange(start, end3);
// "May 16, 2026 – January 10, 2027"  (nothing shared)

There is also formatRangeToParts() that returns typed tokens, with an additional source field ("startRange", "endRange", or "shared") on each part. This is useful for building custom UI for calendar event displays or booking interfaces.

const rangeParts = fmt.formatRangeToParts(start, end);
// [
//   { type: "month", value: "May", source: "shared" },
//   { type: "literal", value: " ", source: "shared" },
//   { type: "day", value: "16", source: "startRange" },
//   { type: "literal", value: " – ", source: "shared" },
//   { type: "day", value: "22", source: "endRange" },
//   { type: "literal", value: ", ", source: "shared" },
//   { type: "year", value: "2026", source: "shared" }
// ]

15 Real-World Code Examples

1. Display a user-friendly timestamp

function friendlyDate(date) {
  return new Intl.DateTimeFormat('en-US', {
    dateStyle: 'medium',
    timeStyle: 'short'
  }).format(date);
}
friendlyDate(new Date());
// "May 16, 2026, 10:30 AM"

2. Format for an API (ISO 8601-like with timezone)

function apiDate(date, tz) {
  return new Intl.DateTimeFormat('sv-SE', {
    timeZone: tz,
    year: 'numeric', month: '2-digit', day: '2-digit',
    hour: '2-digit', minute: '2-digit', second: '2-digit',
    hourCycle: 'h23'
  }).format(date);
}
apiDate(new Date(), 'UTC');
// "2026-05-16 14:30:00"  (sv-SE uses ISO-like separators natively)

3. Show time in multiple cities (world clock)

const cities = [
  { name: 'New York',  tz: 'America/New_York' },
  { name: 'London',    tz: 'Europe/London' },
  { name: 'Mumbai',    tz: 'Asia/Kolkata' },
  { name: 'Tokyo',     tz: 'Asia/Tokyo' },
  { name: 'Sydney',    tz: 'Australia/Sydney' },
];

const now = new Date();
cities.forEach(({ name, tz }) => {
  const time = new Intl.DateTimeFormat('en-US', {
    timeZone: tz,
    hour: 'numeric',
    minute: '2-digit',
    timeZoneName: 'short'
  }).format(now);
  console.log(`${name}: ${time}`);
});
// New York: 10:30 AM EDT
// London: 3:30 PM BST
// Mumbai: 8:00 PM IST
// Tokyo: 11:30 PM JST
// Sydney: 12:30 AM AEST

4. Get just the month name

function monthName(date, locale = 'en-US') {
  return new Intl.DateTimeFormat(locale, { month: 'long' }).format(date);
}
monthName(new Date('2026-05-16'));       // "May"
monthName(new Date('2026-05-16'), 'fr'); // "mai"
monthName(new Date('2026-05-16'), 'ja'); // "5月"

5. Get the day of the week

function dayOfWeek(date, locale = 'en-US') {
  return new Intl.DateTimeFormat(locale, { weekday: 'long' }).format(date);
}
dayOfWeek(new Date('2026-05-16'));         // "Saturday"
dayOfWeek(new Date('2026-05-16'), 'de');   // "Samstag"
dayOfWeek(new Date('2026-05-16'), 'hi');   // "शनिवार"

6. Format with milliseconds

new Intl.DateTimeFormat('en-US', {
  hour: 'numeric', minute: '2-digit', second: '2-digit',
  fractionalSecondDigits: 3,
  hourCycle: 'h23'
}).format(new Date());
// "14:30:45.123"

7. 24-hour time in any locale

new Intl.DateTimeFormat('en-US', {
  hour: '2-digit', minute: '2-digit',
  hourCycle: 'h23'
}).format(new Date());
// "14:30" (instead of "2:30 PM")

8. Relative time labels (today/yesterday)

function relativeDay(date) {
  const now = new Date();
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const target = new Date(date.getFullYear(), date.getMonth(), date.getDate());
  const diff = Math.round((today - target) / 86400000);

  if (diff === 0) return 'Today';
  if (diff === 1) return 'Yesterday';
  if (diff === -1) return 'Tomorrow';
  if (diff > 1 && diff < 7) return `${diff} days ago`;

  return new Intl.DateTimeFormat('en-US', {
    dateStyle: 'medium'
  }).format(date);
}
// For recent dates: "Today", "Yesterday", "3 days ago"
// For older dates: "May 10, 2026"

9. Islamic calendar

new Intl.DateTimeFormat('ar-SA', {
  calendar: 'islamic',
  dateStyle: 'full'
}).format(new Date('2026-05-16'));
// Full Hijri date in Arabic script

10. Japanese Imperial era

new Intl.DateTimeFormat('ja-JP-u-ca-japanese', {
  dateStyle: 'full'
}).format(new Date('2026-05-16'));
// "令和8年5月16日土曜日"

11. Thai Buddhist calendar

new Intl.DateTimeFormat('th-TH', {
  calendar: 'buddhist',
  dateStyle: 'long'
}).format(new Date('2026-05-16'));
// Shows year 2569 (Buddhist Era = Gregorian + 543)

12. Format with Devanagari numerals

new Intl.DateTimeFormat('hi-IN', {
  numberingSystem: 'deva',
  dateStyle: 'long',
  timeStyle: 'short',
  timeZone: 'Asia/Kolkata'
}).format(new Date());
// Displays Hindi date with Devanagari digits (१६, ८०८६, etc.)

13. Build a UTC offset string programmatically

function getUTCOffset(tz) {
  const parts = new Intl.DateTimeFormat('en-US', {
    timeZone: tz, timeZoneName: 'longOffset'
  }).formatToParts(new Date());
  return parts.find(p => p.type === 'timeZoneName')?.value;
}
getUTCOffset('Asia/Kolkata');       // "GMT+05:30"
getUTCOffset('America/New_York');   // "GMT-04:00" or "GMT-05:00"
getUTCOffset('Pacific/Chatham');    // "GMT+12:45"

14. React hook for locale-aware formatting

import { useMemo, useCallback } from 'react';

function useDateFormatter(locale, options) {
  const formatter = useMemo(
    () => new Intl.DateTimeFormat(locale, options),
    [locale, JSON.stringify(options)]
  );
  return useCallback((date) => formatter.format(date), [formatter]);
}

// Usage in a component:
function EventCard({ event }) {
  const formatDate = useDateFormatter('en-US', {
    dateStyle: 'medium',
    timeStyle: 'short',
    timeZone: event.timezone
  });
  return <time dateTime={event.date.toISOString()}>
    {formatDate(event.date)}
  </time>;
}

15. Server-side rendering with consistent timezone

// In SSR (Node.js), the server's system timezone differs from
// the user's. Always pass an explicit timeZone to avoid
// hydration mismatch in Next.js, Remix, or Astro:

function renderDate(date, userTimezone) {
  return new Intl.DateTimeFormat('en-US', {
    timeZone: userTimezone, // from user session or cookie
    dateStyle: 'long',
    timeStyle: 'short'
  }).format(date);
}

// Both server and client produce identical output because
// both use the same explicit timezone, not the system default.

Common Mistakes and Fixes

Mistake 1: Mixing dateStyle with component options

// WRONG: throws TypeError
new Intl.DateTimeFormat('en-US', {
  dateStyle: 'long',
  hour: 'numeric'
});

// FIX option A: use timeStyle alongside dateStyle
new Intl.DateTimeFormat('en-US', {
  dateStyle: 'long',
  timeStyle: 'short'
});

// FIX option B: use individual components for everything
new Intl.DateTimeFormat('en-US', {
  year: 'numeric', month: 'long', day: 'numeric',
  hour: 'numeric'
});

Mistake 2: Using offset strings instead of timezone names

// WRONG: "+05:30" is not a valid timeZone value
new Intl.DateTimeFormat('en-US', {
  timeZone: '+05:30'  // RangeError!
});

// FIX: use the IANA timezone name
new Intl.DateTimeFormat('en-US', {
  timeZone: 'Asia/Kolkata'
});

Mistake 3: Assuming Date months are 1-indexed

// The Date constructor uses 0-indexed months:
new Date(2026, 4, 16); // May 16 (month 4 = May, not April)
new Date(2026, 0, 1);  // January 1 (month 0 = January)

// Intl output is always human-readable (1-indexed):
// "May 16, 2026" or "5/16/2026"

// Tip: use ISO strings to avoid confusion:
new Date('2026-05-16'); // May 16 — month is 1-indexed in ISO strings

Mistake 4: Creating a new formatter on every render

// SLOW: creates a new Intl.DateTimeFormat on every iteration
items.forEach(item => {
  const fmt = new Intl.DateTimeFormat('en-US', opts);
  item.displayDate = fmt.format(item.date);
});

// FAST: create once, reuse many times (~18x faster)
const fmt = new Intl.DateTimeFormat('en-US', opts);
items.forEach(item => {
  item.displayDate = fmt.format(item.date);
});

Mistake 5: Relying on Date.toString() for display

// FRAGILE: output format is implementation-defined
new Date().toString();
// "Sat May 16 2026 10:30:00 GMT-0400 (Eastern Daylight Time)"
// This varies between V8, SpiderMonkey, and JavaScriptCore.

// STABLE: Intl produces consistent, spec-driven output
new Intl.DateTimeFormat('en-US', {
  dateStyle: 'full', timeStyle: 'long',
  timeZone: 'America/New_York'
}).format(new Date());
// Always "Saturday, May 16, 2026 at 10:30:00 AM EDT"

Mistake 6: Forgetting that Date objects are always UTC internally

// A Date object does NOT store a timezone.
// It stores milliseconds since the Unix epoch (always UTC).
const d = new Date('2026-05-16T22:00:00+05:30');
console.log(d.toISOString());
// "2026-05-16T16:30:00.000Z" — stored as UTC

// The timeZone option changes the *display*, not the Date:
new Intl.DateTimeFormat('en-US', {
  timeZone: 'Asia/Kolkata', timeStyle: 'short'
}).format(d);
// "10:00 PM"

new Intl.DateTimeFormat('en-US', {
  timeZone: 'UTC', timeStyle: 'short'
}).format(d);
// "4:30 PM"

Mistake 7: Using deprecated timezone identifiers

// These old names still work but are deprecated:
// "US/Eastern"       → use "America/New_York"
// "Asia/Calcutta"    → use "Asia/Kolkata"
// "Pacific/Samoa"    → use "Pacific/Pago_Pago"
// "GB"               → use "Europe/London"

// Always use the canonical IANA name.
// List them all:
const allZones = Intl.supportedValuesOf('timeZone');
console.log(allZones.length); // ~400+

Performance Best Practices

Creating an Intl.DateTimeFormat instance is expensive relative to calling .format() on an existing one. The constructor resolves the locale, loads ICU data, and compiles the format pattern. In benchmarks, constructor time is roughly 10-50x the cost of a single .format() call.

// Benchmark: constructor per call vs. reuse
console.time('constructor');
for (let i = 0; i < 10000; i++) {
  new Intl.DateTimeFormat('en-US', { dateStyle: 'long' }).format(new Date());
}
console.timeEnd('constructor');
// ~150ms

console.time('reuse');
const fmt = new Intl.DateTimeFormat('en-US', { dateStyle: 'long' });
for (let i = 0; i < 10000; i++) {
  fmt.format(new Date());
}
console.timeEnd('reuse');
// ~8ms  (roughly 18x faster)

Best practices:

For a utility to manage formatter caching automatically, use a factory pattern:

const formatters = new Map();

function getFormatter(locale, options) {
  const key = locale + JSON.stringify(options);
  if (!formatters.has(key)) {
    formatters.set(key, new Intl.DateTimeFormat(locale, options));
  }
  return formatters.get(key);
}

// Usage — formatter is created once and cached:
getFormatter('en-US', { dateStyle: 'long' }).format(someDate);
getFormatter('en-US', { dateStyle: 'long' }).format(otherDate); // cache hit

Note: Date.prototype.toLocaleString(locale, options) internally creates a new Intl.DateTimeFormat on every call. If you call toLocaleString in a loop, you pay the constructor cost every iteration. Switching to an explicit, cached Intl.DateTimeFormat instance can speed up table rendering by an order of magnitude.

Frequently Asked Questions

What is Intl.DateTimeFormat in JavaScript?

Intl.DateTimeFormat is a built-in constructor that creates objects for language-sensitive date and time formatting. It uses the IANA timezone database and ICU locale data built into the JavaScript engine, giving you correct timezone conversions and locale-specific formatting without any external library.

How do I convert a time to a different timezone?

Pass the timeZone option with an IANA timezone name: new Intl.DateTimeFormat('en-US', { timeZone: 'Asia/Kolkata', dateStyle: 'full', timeStyle: 'long' }).format(new Date()). This formats the current moment as it appears in Indian Standard Time, regardless of where the code runs.

Can I mix dateStyle with individual component options?

No. If you pass dateStyle or timeStyle alongside individual options like month or hour, a TypeError is thrown. Use one approach or the other. You can combine dateStyle/timeStyle with structural options like timeZone and hourCycle.

How does it handle Daylight Saving Time?

Automatically. When you specify a timezone like America/New_York, the engine checks the IANA timezone database for the given date and applies the correct offset (EST or EDT). You never calculate DST offsets manually.

What is the difference between format() and formatToParts()?

format() returns a single string. formatToParts() returns an array of objects with type and value fields, letting you rearrange, style, or filter individual components for custom output.

How do I format dates in Asia/Kolkata (IST)?

Use new Intl.DateTimeFormat('en-IN', { timeZone: 'Asia/Kolkata', dateStyle: 'full', timeStyle: 'long' }).format(new Date()). The en-IN locale gives Indian English formatting, and Asia/Kolkata is the IANA identifier for IST (UTC+5:30).

How do I convert 10 PM IST to UTC?

Format the same Date object with two formatters: one with timeZone: 'Asia/Kolkata' and one with timeZone: 'UTC'. Since IST is UTC+5:30, 10:00 PM IST = 4:30 PM UTC. A JavaScript Date always holds a UTC instant internally, so both formatters display the same moment in different timezones.

Is Intl.DateTimeFormat supported in all browsers?

The core API works in Chrome 24+, Firefox 29+, Safari 10+, Edge 12+, and Node.js 0.12+. Newer features like dateStyle/timeStyle need Chrome 76+, Firefox 79+, Safari 14.1+. See the full browser support table above.

Do I still need Moment.js or date-fns for formatting?

For formatting and timezone display, Intl.DateTimeFormat replaces both libraries entirely. For date arithmetic (adding days, diffing months, parsing arbitrary strings), you may still benefit from a library like date-fns, luxon, or the upcoming TC39 Temporal API. Moment.js is in maintenance mode and should not be used in new projects.

How do I list all available IANA timezones?

In modern engines: Intl.supportedValuesOf('timeZone') returns a sorted array of all supported IANA timezone identifiers (400+). Available in Chrome 93+, Firefox 93+, Safari 15.4+, Node.js 18+.

Related Tools and Guides