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.

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

Full production version

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']

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');

Common pitfalls

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.