Sort Dates Correctly Across Locales

To sort dates reliably no matter the user's locale, order them by their absolute instant — date.getTime() or Temporal.Instant.compare — and apply locale formatting only afterward. Part of Locale-Sensitive Date Comparison and Sorting.

Why this scenario is tricky

The trap is sorting the values you can see. A locale-formatted date like 15/03/2024 (en-GB) or 03/15/2024 (en-US) is just text, and Array.prototype.sort with localeCompare orders text alphabetically. Day-first text sorts 01/04 before 15/03 before 28/02 — chronologically backwards. Worse, the result depends on the locale used to format: the same array reorders when a German user (15.03.2024) and a US user (3/15/2024) view it, and month-name locales (15 mars) sort by the spelling of the month. None of these correspond to real time order.

The fix is to recognize that chronological order is a numeric property of the absolute instant. Every Date already stores epoch milliseconds; every Temporal.Instant stores epoch nanoseconds. Comparing those numbers is locale-independent, host-zone-independent, and deterministic. Formatting is a one-way projection for the eyes — you sort the data, then format the sorted data, never the reverse.

Sort the instants, not the labelsDisplay strings sort wrongSort the instants, not the labelssort by '15.03.2024' textlexicographic, locale-brokensort by epoch / ISOtrue chronological orderLocalized strings vary in field order, so sorting them alphabetically scrambles dates.

Minimal working solution

const input = ['2024-03-15T00:00:00Z', '2024-04-01T00:00:00Z', '2024-02-28T00:00:00Z'];

// Compare epoch milliseconds (absolute instant); negative result => a sorts before b.
const sorted = input
  .map((iso) => new Date(iso))
  .sort((a, b) => a.getTime() - b.getTime());

console.log(sorted.map((d) => d.toISOString().slice(0, 10)));
// ['2024-02-28', '2024-03-15', '2024-04-01'] — correct in every locale

The getTime() subtraction is the whole technique: it reduces each date to a number, and numbers have a single, unambiguous order. The same approach with Temporal uses the static comparator directly:

import { Temporal } from '@js-temporal/polyfill';

const instants = input.map((iso) => Temporal.Instant.from(iso));
instants.sort(Temporal.Instant.compare); // returns -1 | 0 | 1 — exactly what sort wants

Sort by absolute valueCompare instants; format only for displaySort by absolute valuedate listsort byepoch msformat eachfor display

Full production version

The key insight for sorting dates "across locales" is that the sort itself is not locale-sensitive — chronological order is universal — and the locale only enters when you display the sorted results. The bug people fear ("dates sort differently in different locales") actually comes from sorting formatted strings, which orders them lexicographically by their localized text and scrambles chronology. The fix is to sort the underlying instants and format only afterward. Comparing epoch values (or Temporal.Instant.compare) gives a stable, correct order that is identical everywhere, and the locale-appropriate rendering is applied to the already-ordered list.

So the production pipeline is: parse each input to an absolute value, sort by that value, then map to localized strings for display. Keeping parse-sort-format as three distinct stages means the comparison never touches locale-dependent text and the formatting never influences order. For civil dates without a time, compare PlainDate values with Temporal.PlainDate.compare; for instants, compare the instants. The one genuinely locale- and zone-sensitive decision is which calendar day an instant falls on if you are grouping by day — that grouping should use the viewer's zone — but the chronological sort of the instants themselves is absolute.

This handles Date, ISO strings, and Temporal inputs, validates each value, and supports ascending or descending order. It never touches a formatter, so its output order is identical on any host.

import { Temporal } from '@js-temporal/polyfill';

type DateLike = Date | string | Temporal.Instant | Temporal.ZonedDateTime;
type Direction = 'asc' | 'desc';

// Reduce any supported input to epoch nanoseconds (BigInt) — the absolute instant.
function toEpochNs(value: DateLike): bigint {
  if (value instanceof Temporal.Instant) return value.epochNanoseconds;
  if (value instanceof Temporal.ZonedDateTime) return value.epochNanoseconds;
  if (value instanceof Date) {
    const ms = value.getTime();
    if (Number.isNaN(ms)) throw new RangeError('Invalid Date passed to sortDates');
    return BigInt(ms) * 1_000_000n; // milliseconds -> nanoseconds
  }
  if (typeof value === 'string') {
    // Instant.from requires an offset or trailing Z; it throws on offset-less strings.
    return Temporal.Instant.from(value).epochNanoseconds;
  }
  throw new TypeError(`Unsupported value: ${String(value)}`);
}

export function sortDates<T extends DateLike>(items: readonly T[], dir: Direction = 'asc'): T[] {
  const sign = dir === 'asc' ? 1n : -1n;
  return items
    .map((item) => ({ item, key: toEpochNs(item) })) // compute key once per item
    .sort((x, y) => Number((x.key < y.key ? -1n : x.key > y.key ? 1n : 0n) * sign))
    .map((entry) => entry.item);
}

// Display step is separate: sort first, then format with an explicit timeZone + locale.
const fmt = new Intl.DateTimeFormat('en-GB', { timeZone: 'UTC', dateStyle: 'medium' });
const rows = ['2024-04-01T00:00:00Z', '2024-02-28T00:00:00Z', '2024-03-15T00:00:00Z'];
console.log(sortDates(rows).map((iso) => fmt.format(new Date(iso))));
// ['28 Feb 2024', '15 Mar 2024', '1 Apr 2024']

Stable locale-aware sortNormalize to Instant, sort, then localize outputStable locale-aware sortmixed inputsto InstantTemporal.Instantcompareformat(locale)

Verification snippet

import { Temporal } from '@js-temporal/polyfill';

const data = ['2024-04-01T00:00:00Z', '2024-02-28T00:00:00Z', '2024-03-15T00:00:00Z'];

// Right: numeric epoch order is the chronological truth.
const right = [...data].sort(
  (a, b) => new Date(a).getTime() - new Date(b).getTime()
);

// Wrong: localeCompare on de-DE-formatted strings reorders by glyphs.
const deFmt = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', dateStyle: 'short' });
const wrong = [...data].sort((a, b) =>
  deFmt.format(new Date(a)).localeCompare(deFmt.format(new Date(b)))
);

console.assert(
  JSON.stringify(right) === JSON.stringify([
    '2024-02-28T00:00:00Z',
    '2024-03-15T00:00:00Z',
    '2024-04-01T00:00:00Z',
  ]),
  'epoch sort must be chronological'
);

// '01.03'-style day/month text sorts differently from the correct instant order.
console.assert(
  JSON.stringify(wrong) !== JSON.stringify(right),
  'string sort is expected to disagree with chronological order'
);

// Locale-independence: sorting again under a different formatter does not change order.
const enFmt = new Intl.DateTimeFormat('en-US', { timeZone: 'UTC' });
const rightAgain = [...data].sort(
  (a, b) => Temporal.Instant.from(a).epochNanoseconds < Temporal.Instant.from(b).epochNanoseconds ? -1 : 1
);
void enFmt; // formatter unused for ordering — order is identical regardless of locale
console.assert(JSON.stringify(rightAgain) === JSON.stringify(right), 'order is locale-independent');

Sort assertionsChronological order regardless of display localeAssertions that prove the edge casesort instantsascending timeen-US vs de-DEsame ordertiesstablemixed zonesby absolute time

Common pitfalls

The dominant pitfall is sorting formatted date strings, which orders them by localized text (so "avril" sorts before "mars", or "01/02" is ambiguous) rather than by time. Sort the underlying instants or PlainDate values and format afterward. The second is comparing dates with </> on Date objects and relying on implicit coercion, which works for Date but throws for Temporal types and hides intent; use the explicit compare static. The third is mixing types in the comparator — a Date against a PlainDate — which drags in coercion and a time-of-day; keep every value in one comparable type.

A fourth pitfall is assuming the sort needs to be localized at all; chronological order is universal, and only display is locale-sensitive, so a "locale-aware sort" that reorders by locale is almost always a bug. A fifth is grouping sorted results by calendar day without choosing a zone, so the day boundaries follow the host zone rather than the viewer's; reduce to PlainDate in the viewer's zone before grouping. Separating the absolute sort from the locale-aware display resolves all of these.

Sorting pitfallsSorting on formatted textWrongRightarray.sort() on strings'2/1' before '10/1'compare epoch ms / Instantnumeric, exactcompare toLocaleString()field order varieslocalize after sortingdisplay only

Frequently Asked Questions

Can I just call .sort() on an array of Date objects?

No. The default sort converts elements to strings, so Date objects are sorted by their toString() text, which is host-timezone dependent. Always pass a comparator: (a, b) => a.getTime() - b.getTime().

How do I sort newest-first?

Reverse the comparator: (a, b) => b.getTime() - a.getTime(), or pass 'desc' to the sortDates utility above. The basis is still the numeric epoch — only the sign flips.

Where does locale formatting belong then?

After sorting, at render time only. Build a cached Intl.DateTimeFormat with an explicit timeZone and apply it to the already-sorted array, as shown in the production example.

How do I sort dates correctly across locales in JavaScript?

Sort the underlying absolute values, not formatted strings: compare epoch milliseconds or use Temporal.Instant.compare (or PlainDate.compare for civil dates), then format the already-ordered list for display. Chronological order is universal, so the sort is not locale-sensitive; only the rendering is. Sorting formatted strings is what scrambles order, because it compares localized text lexicographically.

Why does sorting formatted date strings give the wrong order?

Because string comparison is lexicographic over localized text, not chronological: 'avril' sorts before 'mars', and formats like '01/02/2024' are ambiguous and sort by their characters rather than their dates. Formatting also applies a locale and zone that can reorder values. Always sort the typed instants or PlainDate values and format only after sorting.

Is sorting dates a locale-sensitive operation?

No — chronological order is universal, so the sort itself is not locale-sensitive; only the display of the sorted results is. A 'locale-aware sort' that reorders items by locale is almost always a bug caused by sorting formatted strings. The one place a zone matters is if you group sorted results by calendar day, which should use the viewer's zone.