When you need date formatting in JS—whether for logs, UI labels, or API payloads—reaching for string concatenation on Date getters goes wrong fast across languages, calendars, and time zones. The standard JavaScript date formatter for display strings is Intl.DateTimeFormat: one object captures locale and options, and its format() method turns a Date (or other supported temporal types in newer engines) into a human-readable string.
If you are building the underlying Date first, see how to get the current date in JavaScript and how to add days to a date. For ISO 8601 strings used in APIs and storage (a different problem from locale display), see converting JS dates to ISO date strings.
This guide focuses on Intl.DateTimeFormat, including dateStyle and timeStyle, formatRange, formatToParts, and pitfalls that show up in real apps (including confusion with jQuery Validate).
Tested on: Node.js v20.18.2 with full ICU. Exact strings can vary slightly by ICU version; each example fixes
localeand oftentimeZoneorprocess.env.TZso your run should match the described pattern.
Quick reference
Use this table for date formatting js: style shortcuts vs granular fields and stability.
| Goal | Pattern |
|---|---|
| Locale display | new Intl.DateTimeFormat(locale, options).format(date) |
| Preset length | dateStyle / timeStyle (not mixed with per-field options) |
| Ranges | formatRange(start, end) |
| Stable tests | Fix locale, timeZone, and process.env.TZ when needed |
Intl.DateTimeFormat basics
The Intl.DateTimeFormat constructor takes up to two arguments: a BCP 47 locale (string or array of fallbacks) and an options object. Call format(date) on the instance to obtain the string.
Like Intl.NumberFormat, the formatter resolves a pattern that best matches your options and the locale. You might ask for a sparse set of fields, but the engine can still include related parts if the locale’s pattern requires them for a coherent datetime formatting result.
const d = new Date("2020-01-02T13:14:15.000Z"); // instant: 2 Jan 2020, 13:14:15 UTC
console.log(new Intl.DateTimeFormat("en-US").format(d));
console.log(new Intl.DateTimeFormat("fr-FR").format(d));
const longDate = {
weekday: "long",
month: "long",
year: "numeric",
day: "numeric",
};
console.log(new Intl.DateTimeFormat("en-US", longDate).format(d));
console.log(new Intl.DateTimeFormat("es-ES", longDate).format(d));
const nyTime = {
hour: "numeric",
minute: "2-digit",
timeZone: "America/New_York",
};
console.log(new Intl.DateTimeFormat("fr-CA", nyTime).format(d));You should get five lines: US short numeric date, French day/month order, long English weekday line, long Spanish weekday line, then a Canadian-French time fragment for New York (13:14 UTC → morning Eastern) such as 8 h 14 (exact spacing can vary slightly by ICU).
The first two lines illustrate locale-sensitive order and separators for the same instant. The fr-CA line shows time zone conversion: 13:14 UTC becomes morning in New York, formatted with Canadian French conventions. After you order instants for logic, compare dates in JavaScript with getTime() or operators; use Intl only for the human-readable layer.
Locale defaults without extra options
If you omit granular options, you get each locale’s default numeric-style date (still locale dependent). The following uses a fixed instant in UTC so your JS date formatting experiments match this article.
process.env.TZ = "UTC";
const d = new Date("2022-11-09T12:00:00.000Z");
console.log(new Intl.DateTimeFormat("en-US").format(d));
console.log(new Intl.DateTimeFormat("en-GB").format(d));
console.log(new Intl.DateTimeFormat("ko-KR").format(d));With TZ=UTC and this instant, you should see US month/day/year ordering, British day/month/year ordering, and a Korean-style year-first line—punctuation may differ slightly by ICU, but the field order matches each locale’s default numeric pattern.
dateStyle and timeStyle
For “preset” lengths of date and time, ECMAScript provides dateStyle and timeStyle. Each accepts only "full", "long", "medium", or "short"—not values like "numeric" (those belong to per-field options such as year: "numeric").
They are mutually exclusive with individual field options (weekday, month, hour, and so on). Mixing them triggers a TypeError (see below).
process.env.TZ = "Europe/London";
const d = new Date("2022-11-09T14:04:00"); // local wall-clock in London
console.log(
new Intl.DateTimeFormat("en-GB", {
dateStyle: "full",
timeStyle: "short",
}).format(d),
);With TZ=Europe/London and the given local wall time, en-GB + dateStyle: "full" + timeStyle: "short" typically prints a single line like Wednesday 9 November 2022 at 14:04 (comma or “at” wording follows locale data).
For quick calls without storing a formatter, Date.prototype.toLocaleString accepts the same arguments:
process.env.TZ = "UTC";
const d = new Date("2022-11-09T12:00:00.000Z");
console.log(d.toLocaleString("en-GB", { dateStyle: "medium", timeStyle: "short" }));With TZ=UTC, you should see a compact British-style datetime string for noon UTC on 9 Nov 2022, such as 9 Nov 2022, 12:00.
formatRange for concise intervals
formatRange(startDate, endDate) prints the shortest clear range for the configured locale and options—useful for bookings, calendars, and filters. If your source data is an unsorted array of Date values, sort by date in JavaScript before you build range labels.
const formatter = new Intl.DateTimeFormat("en", { dateStyle: "long" });
const christmas = new Date(1999, 11, 24);
const newYearsEve = new Date(1999, 11, 31);
const newYearsDay = new Date(2000, 0, 1);
console.log(formatter.formatRange(christmas, newYearsEve));
console.log(formatter.formatRange(newYearsEve, newYearsDay));Two lines: a merged range across late December (December 24 – 31, 1999 with a range separator character from Unicode), then a range that crosses into January 2000.
formatToParts and resolvedOptions
When you need a custom layout but still want locale-aware pieces, use formatToParts to get labeled segments (day, month, literal, etc.), or resolvedOptions() to see the effective locale, calendar, numbering system, hour cycle, and which style flags won.
const d = new Date("2020-01-02T13:14:15.000Z");
const f = new Intl.DateTimeFormat("en-GB", {
dateStyle: "medium",
timeStyle: "short",
timeZone: "UTC",
});
console.log(JSON.stringify(f.formatToParts(d)));
console.log(JSON.stringify(f.resolvedOptions(), null, 2));formatToParts returns a short array of typed segments (day, literal spaces, month, year, comma, hour, colon, minute). resolvedOptions() prints a JSON object showing locale: "en-GB", calendar: "gregory", timeZone: "UTC", dateStyle / timeStyle, and related hour-cycle flags—useful when debugging formatter configuration.
Common TypeError when mixing styles and fields
If you pass dateStyle or timeStyle together with granular fields, initialization fails:
const d = new Date("2022-11-09T12:00:00.000Z");
try {
new Intl.DateTimeFormat("en", { dateStyle: "full", weekday: "long" }).format(d);
} catch (e) {
console.log(e.toString());
}The catch logs a TypeError whose message explains that weekday cannot be combined with dateStyle (wording may vary slightly by engine).
Pick either the style shortcuts or explicit year, month, day, hour, and similar options. Similar “invalid combination” mistakes show up elsewhere when APIs throw—see how to throw and handle errors in JavaScript if you are wiring this into validation flows.
Node.js, browsers, and reproducibility
Modern Node.js builds ship with full ICU data, so Intl.DateTimeFormat behaves like a capable browser for most sites. Strings can still vary by ICU version and default locale, so for tests and docs fix locales and, when needed, set process.env.TZ (as in the snippets above) instead of relying on the host’s zone. For how new Date interacts with the OS clock and zones in Node, read why Node’s Date constructor can look “wrong” at a glance.
Summary
JavaScript datetime formatting for display belongs in Intl.DateTimeFormat: pass locales and either dateStyle / timeStyle or fine-grained field options (not both), add timeZone when the wall clock must reflect a specific region, and use formatRange, formatToParts, and resolvedOptions() when you need ranges, custom assembly, or debugging. Pair this with proper parsing or validation for user input—Intl is a formatter, not a validator—so tools like jQuery Validate stay on the parsing/rule side while Intl handles the readable output.
References
MDN and ECMA-402 references for Intl.DateTimeFormat and internationalization.
Intl.DateTimeFormat - JavaScript | MDN
Internationalization - JavaScript | MDN
ECMA-402 DateTimeFormat objects
