Dates, times and formatting with Intl

Create and compare dates without the classic Date traps, format dates, numbers and currencies for any locale, and meet the Temporal API.

  • Beginner
  • 7 min read
  • Updated 17 Oct 2026

What you will learn

  • Format dates for a locale
  • Avoid common time zone bugs
  • Format numbers and currencies with Intl.NumberFormat
  • Recognise where Temporal improves on Date

Before you start

Dates look simple until a user in another time zone sees yesterday's date, or a month comes out one too high. This lesson covers the Date object and its traps, then the Intl APIs that format dates, numbers and currencies correctly for any language. It ends with Temporal, the newer date API designed to replace Date.

What a Date really is

A Date holds a single number: milliseconds since 1 January 1970 at midnight UTC. Everything else, such as the day, the hour or the month, is calculated from that number using either UTC or the computer's local time zone.

JavaScript
const now = new Date();
console.log(now.getTime()); // → 1789000000000 (a large number; yours will differ)
console.log(Date.now());    // → the same kind of number, without creating a Date

Creating dates

There are three reliable ways:

JavaScript
const fromParts = new Date(2026, 3, 30, 14, 15);  // local time
const fromIso = new Date('2026-04-30T14:15:00Z'); // exact instant, UTC
const fromMs = new Date(0);                         // 1 Jan 1970, 00:00 UTC

Pitfall 1: months start at 0

In new Date(year, month, day), January is 0 and December is 11. So new Date(2026, 3, 30) is 30 April, not 30 March. Days, by contrast, start at 1. getMonth() follows the same rule, so add 1 when you display it yourself.

Pitfall 2: date-only strings are UTC

JavaScript
const a = new Date('2026-04-30');       // date only: treated as UTC midnight
const b = new Date('2026-04-30T00:00'); // date and time, no Z: local midnight

For someone in New York, a is 8pm on 29 April local time. That is why dates from JSON can appear one day early. When a value is a calendar date with no time, such as the updated field in the course dashboard, format it in UTC (shown below) so every user sees the same day.

Avoid other string formats such as '04/30/2026' or '30 April 2026'. How they are parsed is up to each engine.

Pitfall 3: dates are mutable

Setter methods change the original object:

JavaScript
const start = new Date('2026-04-30T09:00:00Z');
const end = start;
end.setUTCDate(end.getUTCDate() + 7);

console.log(start.toISOString()); // → 2026-05-07T09:00:00.000Z (changed too)

Copy first with new Date(start) before changing a date. Setters also roll over sensibly: day 31 in a 30-day month moves into the next month.

Comparing and measuring

Subtracting two dates gives milliseconds:

JavaScript
const release = new Date('2026-04-30T00:00:00Z');
const review = new Date('2026-05-14T00:00:00Z');

const days = (review - release) / (1000 * 60 * 60 * 24);
console.log(days); // → 14

console.log(review > release); // → true
console.log(review.getTime() === new Date('2026-05-14T00:00:00Z').getTime()); // → true

Use getTime() for equality. === compares objects, so two separate Date objects for the same instant are never ===.

Dividing by a day's milliseconds is safe in UTC. In local time, a day across a daylight saving change has 23 or 25 hours, so round the result with Math.round.

Formatting dates with Intl.DateTimeFormat

toISOString() gives a machine-friendly string. For people, use Intl.DateTimeFormat, which knows the date conventions of every locale:

JavaScript
const date = new Date('2026-04-30T14:15:00Z');

const uk = new Intl.DateTimeFormat('en-GB', { dateStyle: 'long', timeZone: 'UTC' });
const us = new Intl.DateTimeFormat('en-US', { dateStyle: 'long', timeZone: 'UTC' });
const de = new Intl.DateTimeFormat('de-DE', { dateStyle: 'long', timeZone: 'UTC' });

console.log(uk.format(date)); // → 30 April 2026
console.log(us.format(date)); // → April 30, 2026
console.log(de.format(date)); // → 30. April 2026

dateStyle and timeStyle accept 'full', 'long', 'medium' and 'short'. For finer control, list the parts you want instead:

JavaScript
const custom = new Intl.DateTimeFormat('en-GB', {
  weekday: 'short',
  day: 'numeric',
  month: 'short',
  hour: '2-digit',
  minute: '2-digit',
  timeZone: 'Europe/London',
});

console.log(custom.format(date)); // → Thu 30 Apr, 15:15

London is on British Summer Time in April, so 14:15 UTC shows as 15:15. The timeZone option takes IANA names such as 'Australia/Sydney' or 'America/New_York'. Leave it out to use the visitor's own time zone, which is usually what you want for times.

Pass undefined as the locale to use the visitor's browser language: new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }). Creating a formatter has a cost, so create it once and reuse it in loops.

date.toLocaleDateString('en-GB', options) is a shortcut that takes the same options.

Relative times

Intl.RelativeTimeFormat writes phrases such as "3 days ago":

JavaScript
const rtf = new Intl.RelativeTimeFormat('en-GB', { numeric: 'auto' });

console.log(rtf.format(-3, 'day'));  // → 3 days ago
console.log(rtf.format(-1, 'day'));  // → yesterday
console.log(rtf.format(2, 'week'));  // → in 2 weeks

You calculate the number and unit; the formatter handles the wording and grammar.

Formatting numbers and currencies

Intl.NumberFormat handles thousands separators, decimals, currencies, percentages and units:

JavaScript
const amount = 1234567.891;

console.log(new Intl.NumberFormat('en-GB').format(amount)); // → 1,234,567.891
console.log(new Intl.NumberFormat('de-DE').format(amount)); // → 1.234.567,891

const gbp = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' });
console.log(gbp.format(49.5)); // → £49.50

const aud = new Intl.NumberFormat('en-AU', { style: 'currency', currency: 'AUD' });
console.log(aud.format(49.5)); // → $49.50

const pct = new Intl.NumberFormat('en-GB', { style: 'percent', maximumFractionDigits: 1 });
console.log(pct.format(0.4567)); // → 45.7%

const compact = new Intl.NumberFormat('en-US', { notation: 'compact' });
console.log(compact.format(12500)); // → 13K

Currency formatting does not convert between currencies; it only displays the amount you give it. Keep money in the smallest unit (pence or cents) as whole numbers for calculations, and divide only when formatting.

Intl.ListFormat and Intl.PluralRules complete the set, turning ['HTML', 'CSS', 'JS'] into "HTML, CSS and JS" and choosing between "1 lesson" and "2 lessons".

Dashboard: formatting the updated date

The course dashboard stores updated as a date-only string. Format it in UTC so it never shifts by a day:

JavaScript
const updatedFormat = new Intl.DateTimeFormat('en-GB', { dateStyle: 'medium', timeZone: 'UTC' });

function formatUpdated(isoDate) {
  return updatedFormat.format(new Date(isoDate));
}

console.log(formatUpdated('2026-04-30')); // → 30 Apr 2026

Temporal: the replacement for Date

Date cannot be fixed without breaking old websites, so a new built-in, Temporal, has been designed alongside it. Its objects are immutable, months start at 1, and it has separate types for separate ideas: Temporal.PlainDate for a calendar date with no time zone, Temporal.Instant for an exact moment, Temporal.ZonedDateTime for a moment in a named time zone, and Temporal.Duration for lengths of time.

JavaScript
if (typeof Temporal !== 'undefined') {
  const updated = Temporal.PlainDate.from('2026-04-30');
  const review = updated.add({ days: 30 });

  console.log(review.toString());   // → 2026-05-30
  console.log(updated.toString());  // → 2026-04-30 (unchanged)
  console.log(updated.month);       // → 4
} else {
  console.log('Temporal is not available in this environment');
}

A PlainDate has no time zone, so the "one day early" bug cannot happen.

Temporal is new. Firefox and Chromium-based browsers (Chrome and Edge) ship it, but Safari did not have it in a stable release at the time of writing, and Node.js 20 does not include it. Check MDN for current support, and use the official polyfill if you need it everywhere today. This course uses Date with Intl for the dashboard so the code runs in every current browser.

Summary

  • A Date is a count of milliseconds since 1970 UTC; months in new Date(y, m, d) start at 0.
  • Date-only strings parse as UTC midnight, so format calendar dates with timeZone: 'UTC'.
  • Dates are mutable: copy before changing, and compare with getTime().
  • Use Intl.DateTimeFormat, Intl.RelativeTimeFormat and Intl.NumberFormat for any text people read.
  • Temporal fixes the design problems of Date; check browser support before relying on it.

Further reading