How to Format a Date as dd/mm/yyyy in JavaScript
Format any JavaScript date as dd/mm/yyyy with a five-line function or toLocaleDateString('en-GB'), plus time zones, parsing UK dates and the month-0 trap.
Temporal is JavaScript’s new built-in date and time API, designed to replace the old Date object. Instead of one confusing type, it gives you separate, immutable types for each job: Temporal.PlainDate for a calendar date such as a birthday, Temporal.ZonedDateTime for an exact moment in a named time zone, Temporal.Duration for lengths of time, and Temporal.Instant for a timestamp. Months are numbered 1 to 12, adding a month to 31 January gives 28 February instead of 2 March, and time zones are handled properly.
Temporal is trending because it has finally shipped in mainstream runtimes. Node.js 26.0.0, released on 5 May 2026, turned it on by default, and Node 26 is due to become the long-term support (LTS) release this October. Chrome 144 (13 January 2026) and Firefox 139 (27 May 2025) also support it. I ran every example below on Node.js 26.11.1 on 12 October 2026 and pasted the real output.
Date has been in JavaScript since 1995 and has well-known problems:
new Date(2026, 0, 31) is 31 January, not a mistake you’d guess.setMonth() changes the object in place, so passing a date to a function can silently change it for the caller.Date only knows UTC and the computer’s local zone. Showing a meeting time in London and Dhaka needs extra libraries.Date:const old = new Date(2026, 0, 31);
old.setMonth(old.getMonth() + 1);
console.log(old.toISOString().slice(0, 10));
2026-03-02
There’s no 31 February, so Date rolls over into March. Temporal’s answer to the same question is coming up below. If you just need to format an existing Date, my guide to formatting a date as dd/mm/yyyy in JavaScript still works; Temporal is for when you need real date maths.
Temporal is a global object, like Math. Feature-detect it before relying on it:
console.log(typeof globalThis.Temporal);
On Node.js 26 this printed object. On the box’s older Node.js 20 it printed undefined. Browser support, from the web-features explorer on 12 October 2026, is:
For public websites, that Safari gap matters. Use a polyfill such as @js-temporal/polyfill or temporal-polyfill when globalThis.Temporal is missing, or keep Temporal to server-side code on Node 26 for now.
Picking the right type is most of the work. Here’s the short version:
Temporal.PlainDate: a calendar date with no time or zone. Birthdays, deadlines, bank holidays.Temporal.PlainTime: a wall-clock time with no date. Opening hours, alarm times.Temporal.PlainDateTime: a date and time with no zone. Rarely the right choice for real events, but useful for form inputs before you know the zone.Temporal.ZonedDateTime: an exact moment plus a time zone such as Europe/London. Meetings, flights, anything that happens at a real moment for people in a place.Temporal.Instant: an exact moment with no zone, like a timestamp in a database.Temporal.Duration: a length of time, like “2 months and 2 days”.Temporal.PlainYearMonth and Temporal.PlainMonthDay: “February 2026” or “25 December” on their own.Create a date from an ISO string (YYYY-MM-DD) and read its parts:
const d = Temporal.PlainDate.from("2026-10-12");
console.log(d.toString(), d.year, d.month, d.day, d.dayOfWeek, d.daysInMonth, d.inLeapYear);
2026-10-12 2026 10 12 1 31 false
October is month 10, not 9. dayOfWeek runs from 1 (Monday) to 7 (Sunday), so 12 October 2026 is a Monday. Useful properties like daysInMonth and inLeapYear are built in.
To get today’s date in the computer’s time zone, use Temporal.Now.plainDateISO(). For the current exact moment, use Temporal.Now.instant().
with() returns a copy with some fields replaced:
console.log(d.with({ day: 1 }).toString(), d.with({ day: d.daysInMonth }).toString());
2026-10-01 2026-10-31
Temporal objects are immutable: add() and subtract() return a new object and leave the original alone.
const start = Temporal.PlainDate.from("2026-10-12");
const later = start.add({ days: 7 });
console.log(start.toString(), later.toString());
2026-10-12 2026-10-19
Now the same month problem that tripped up Date:
const jan31 = Temporal.PlainDate.from("2026-01-31");
console.log(jan31.add({ months: 1 }).toString());
try {
jan31.add({ months: 1 }, { overflow: "reject" });
} catch (e) {
console.log(e.constructor.name + ":", e.message);
}
2026-02-28
RangeError: Temporal error: not a valid ISO date.
By default Temporal constrains the result to the last valid day, 28 February, which is what most people expect for “one month later”. If you’d rather catch the problem, pass { overflow: "reject" } and you get a RangeError instead. The exact error wording comes from Node’s engine and may differ in browsers.
until() returns a Temporal.Duration. By default the largest unit for dates is days, but you can ask for months or years:
const exam = Temporal.PlainDate.from("2026-12-14");
console.log(start.until(exam).toString(), start.until(exam, { largestUnit: "months" }).toString(), start.until(exam).days);
P63D P2M2D 63
P63D is the ISO 8601 way of writing “63 days”, and P2M2D is “2 months, 2 days”. Read the parts with properties such as .days and .months.
Age calculations are notoriously fiddly with Date. With Temporal, ask for years:
const birth = Temporal.PlainDate.from("2004-02-29");
console.log(birth.until(Temporal.PlainDate.from("2026-10-12"), { largestUnit: "years" }).years);
22
Even a leap-day birthday works without special-casing.
Use Temporal.PlainDate.compare as a sort function and equals() for equality. Don’t use ===, which compares object identity, not dates:
const dates = ["2026-12-01", "2026-01-15", "2026-06-30"].map(s => Temporal.PlainDate.from(s));
console.log(dates.sort(Temporal.PlainDate.compare).map(String).join(", "), start.equals(Temporal.PlainDate.from("2026-10-12")));
2026-01-15, 2026-06-30, 2026-12-01 true
For skipping weekends and bank holidays, the logic in my post on counting UK working days in JavaScript becomes simpler with dayOfWeek and add({ days: 1 }).
This is where Temporal really shines. Create a moment in a named IANA time zone, then view it in other zones:
const meet = Temporal.ZonedDateTime.from("2026-10-12T15:00[Europe/London]");
console.log(meet.toString());
console.log(meet.withTimeZone("Asia/Dhaka").toString());
console.log(meet.withTimeZone("America/New_York").toPlainTime().toString());
2026-10-12T15:00:00+01:00[Europe/London]
2026-10-12T20:00:00+06:00[Asia/Dhaka]
10:00:00
A 3 pm meeting in London (on British Summer Time, +01:00) is 8 pm in Dhaka and 10 am in New York. The string keeps both the offset and the zone name, so it can be stored and read back without losing information.
UK clocks go back on Sunday 25 October 2026. Watch what happens when you add “1 day” versus “24 hours” across that change:
const beforeDst = Temporal.ZonedDateTime.from("2026-10-24T12:00[Europe/London]");
console.log(beforeDst.add({ days: 1 }).toString(), beforeDst.add({ hours: 24 }).toString());
2026-10-25T12:00:00+00:00[Europe/London] 2026-10-25T11:00:00+00:00[Europe/London]
Adding a day keeps the same wall-clock time (noon). Adding 24 hours lands at 11:00, because that particular day was 25 hours long. Temporal makes you choose which one you mean, which is exactly the kind of bug Date hides. The same issue exists on the server side; I covered it for PHP in setting the timezone to Europe/London in PHP.
Temporal.Duration represents a length of time. It doesn’t automatically “balance” units, but round() and total() will:
const dur = Temporal.Duration.from({ hours: 1, minutes: 90 });
console.log(dur.toString(), dur.round({ largestUnit: "hours" }).toString(), dur.total({ unit: "minutes" }));
PT1H90M PT2H30M 150
PT1H90M is kept exactly as you wrote it, rounding gives PT2H30M (2 hours 30 minutes), and total() converts it to a single number of minutes.
The other Plain types follow the same pattern:
console.log(Temporal.PlainTime.from("09:15").add({ minutes: 50 }).toString());
console.log(Temporal.PlainDateTime.from("2026-10-12T23:30").add({ hours: 1 }).toString());
console.log(Temporal.PlainYearMonth.from("2026-02").daysInMonth, Temporal.PlainMonthDay.from("12-25").toPlainDate({ year: 2026 }).dayOfWeek);
10:05:00
2026-10-13T00:30:00
28 5
A 50-minute lesson starting at 09:15 ends at 10:05. Adding an hour to 23:30 rolls into the next day. February 2026 has 28 days, and Christmas Day 2026 is a Friday (day 5).
You’ll be mixing old and new code for a while, so conversions matter. An Instant maps cleanly to and from epoch milliseconds, and Date has a new toTemporalInstant() method:
const inst = Temporal.Instant.fromEpochMilliseconds(Date.UTC(2026, 9, 12, 9, 30));
console.log(inst.toString(), inst.epochMilliseconds, new Date(inst.epochMilliseconds).toISOString());
console.log(new Date("2026-10-12T09:30:00Z").toTemporalInstant().toString());
2026-10-12T09:30:00Z 1791797400000 2026-10-12T09:30:00.000Z
2026-10-12T09:30:00Z
Note the old Date.UTC(2026, 9, ...) needs month 9 for October. That’s the zero-based month problem again, and one more reason to move to Temporal.
Every Temporal type has toLocaleString(), which uses the same Intl options you may know from Date:
console.log(d.toLocaleString("en-GB", { dateStyle: "full" }));
console.log(meet.toLocaleString("en-GB", { dateStyle: "medium", timeStyle: "short" }));
Monday, 12 October 2026
12 Oct 2026, 15:00
One gotcha I hit: combining dateStyle or timeStyle with individual options such as timeZoneName throws TypeError: Invalid option. That’s an Intl.DateTimeFormat rule, not a Temporal bug. Use either the style shortcuts or individual fields like { day: "numeric", month: "long", timeZoneName: "short" }.
Temporal refuses to parse impossible ISO strings and non-ISO formats:
try { Temporal.PlainDate.from("2026-02-30", { overflow: "reject" }); }
catch (e) { console.log(e.constructor.name + ":", e.message); }
try { Temporal.PlainDate.from("12/10/2026"); }
catch (e) { console.log(e.constructor.name + ":", e.message); }
RangeError: Temporal error: Parsed day value not in a valid range.
RangeError: Temporal error: Invalid character while parsing year value.
In my tests, "2026-02-30" was rejected even without the overflow option, because ISO strings are validated strictly. If you build a date from separate fields, though, the default overflow: "constrain" clamps it: Temporal.PlainDate.from({ year: 2026, month: 2, day: 30 }) gave 2026-02-28. For user input from a form, parse the parts yourself and pass { overflow: "reject" } so mistakes are caught rather than silently adjusted.
"12/10/2026" fails outright. That’s a feature: it’s ambiguous (12 October or 10 December?), and Temporal won’t guess.
Skipping weekends is a loop over dayOfWeek (Saturday is 6, Sunday is 7):
function addWorkingDays(date, n) {
let d = date;
while (n > 0) {
d = d.add({ days: 1 });
if (d.dayOfWeek <= 5) n--;
}
return d;
}
const friday = Temporal.PlainDate.from("2026-10-16");
console.log(friday.dayOfWeek, addWorkingDays(friday, 3).toString());
5 2026-10-21
Three working days after Friday 16 October is Wednesday 21 October. Because add() never mutates, there’s no risk of changing the caller’s friday by accident.
Here’s a countdown from 09:15 in Dhaka to a launch at 18:00 in London. My first attempt threw an error worth knowing about:
RangeError: Temporal error: Timezones must be the same if unit is a day unit.
Days can be 23 or 25 hours long depending on the zone, so Temporal refuses to count days between two different zones. Convert one side first:
function countdown(now, deadline) {
const left = now.until(deadline.withTimeZone(now.timeZoneId), { largestUnit: "days", smallestUnit: "minutes" });
return `${left.days}d ${left.hours}h ${left.minutes}m`;
}
const now = Temporal.ZonedDateTime.from("2026-10-12T09:15[Asia/Dhaka]");
const launch = Temporal.ZonedDateTime.from("2026-10-20T18:00[Europe/London]");
console.log(countdown(now, launch));
console.log(JSON.stringify({ launch }));
8d 13h 45m
{"launch":"2026-10-20T18:00:00+01:00[Europe/London]"}
JSON.stringify uses the full string with offset and zone, and Temporal.ZonedDateTime.from() reads it back to an equal value, so storing Temporal values in JSON or a text column is lossless.
ZonedDateTime.=== or <. Use equals() and compare().round() or total().RangeError by type, not by message text.withTimeZone() first, as in the countdown example.Instant (or a ZonedDateTime string) so the exact moment is never lost.Temporal is a built-in global object that provides modern, immutable date and time types, including PlainDate, ZonedDateTime, Instant and Duration. It’s designed to replace the older Date object.
Yes. Node.js 26.0.0, released on 5 May 2026, enables Temporal by default with no flag. Node.js 20 and older don’t have it, so typeof Temporal returns "undefined" there.
As of 12 October 2026, Chrome and Edge 144+ and Firefox 139+ support it. Safari on macOS and iOS doesn’t yet, so use a polyfill for public websites.
PlainDate is just a calendar date with no time or time zone, like a birthday. ZonedDateTime is an exact moment in a specific time zone, such as a meeting at 15:00 in Europe/London.
Use Temporal.PlainDate.from("2026-01-31").add({ months: 1 }), which returns 28 February 2026. Pass { overflow: "reject" } if you’d rather get an error than an adjusted date.
Call date.toTemporalInstant() to get a Temporal.Instant, then instant.toZonedDateTimeISO("Europe/London") if you need a time zone. To go back, use new Date(instant.epochMilliseconds).
No. Every Temporal object is immutable. Methods such as add(), subtract() and with() return a new object and leave the original unchanged.
// note
This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.
If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.
Format any JavaScript date as dd/mm/yyyy with a five-line function or toLocaleDateString('en-GB'), plus time zones, parsing UK dates and the month-0 trap.
Count and add working days in JavaScript with UTC dates, skip weekends and UK bank holidays from gov.uk's JSON, and avoid the clocks-change bug.
Use Europe/London in PHP the safe way: date_default_timezone_set, DateTimeImmutable, GMT vs BST, store UTC, display dd/mm/YYYY, and avoid DST surprises.
structuredClone(obj) makes a true deep copy that keeps Dates, Maps, Sets and circular references. When spread, Object.assign and JSON.stringify break, and how to copy classes.