Dates and times
Dates are everywhere: when a post was published, how many days until an exam, a person's age, a booking calendar, "last seen 5 minutes ago". JavaScript's built-in Date object represents a single moment in time, and comes with methods to read its parts, change them, compare moments and format them for people. It has a few famous quirks, which this lesson points out as they come up.
A date is a moment, stored as a number
Inside, a Date is just the number of milliseconds since 1 January 1970, 00:00 UTC (the "Unix epoch"). Everything else (year, month, day, hour in your time zone) is calculated from that number:
const moon = new Date(Date.UTC(1969, 6, 20, 20, 17)); // the Moon landing, in UTC
console.log(moon.getTime()); // milliseconds since 1970 (negative: it's before)
console.log(moon.toISOString()); // the standard text form, in UTC
console.log(typeof Date.now()); // the current moment as a numberCreating dates
| You write | You get |
|---|---|
new Date() | right now |
new Date(2026, 9, 31) | 31 October 2026, 00:00 local time (month 9 is October!) |
new Date(2026, 9, 31, 14, 30) | the same day at 14:30 local time |
new Date("2026-10-31T14:30:00") | from ISO text, local time |
new Date("2026-10-31") | from a date-only ISO text: midnight UTC |
new Date(1798675200000) | from a timestamp in milliseconds |
Date.UTC(2026, 9, 31) | the timestamp of a UTC moment (a number) |
const exam = new Date(2026, 2, 14, 9, 30); // 14 March 2026, 09:30
console.log(exam.getFullYear(), exam.getMonth(), exam.getDate(), exam.getHours(), exam.getMinutes());
console.log(exam.toDateString());Text in other formats ("31/10/2026", "Oct 31 2026") is parsed differently by different browsers, or not at all. Only the ISO format YYYY-MM-DD (optionally THH:MM:SS) is reliable.
Reading the parts
| Method | Gives | For 14 March 2026, 09:30 |
|---|---|---|
getFullYear() | the year | 2026 |
getMonth() | the month, 0 to 11 | 2 |
getDate() | the day of the month, 1 to 31 | 14 |
getDay() | the day of the week, 0 = Sunday to 6 | 6 (Saturday) |
getHours(), getMinutes(), getSeconds() | the time | 9, 30, 0 |
getTime() | milliseconds since 1970 |
const d = new Date(2026, 2, 14, 9, 30);
const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
console.log(`${days[d.getDay()]}, day ${d.getDate()} of month ${d.getMonth() + 1}`);
const pad = (n) => String(n).padStart(2, "0");
console.log(`${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()}`); // DD/MM/YYYY
console.log(`${pad(d.getHours())}:${pad(d.getMinutes())}`);Every getter has a UTC twin (getUTCHours, getUTCDate...) that reads the time in UTC instead of the visitor's time zone.
Formatting for people
toLocaleDateString and toLocaleTimeString format a date the way a country writes it. Always pass the locale ("en-IN", "en-US", "de-DE") if you want the same result for everyone; without it, each visitor gets their own browser's style:
const d = new Date(2026, 9, 31, 14, 5);
console.log(d.toLocaleDateString("en-IN")); // 31/10/2026
console.log(d.toLocaleDateString("en-US")); // 10/31/2026
console.log(d.toLocaleDateString("en-IN", { dateStyle: "long" }));
console.log(d.toLocaleDateString("en-IN", { weekday: "long", day: "numeric", month: "long", year: "numeric" }));
console.log(d.toLocaleTimeString("en-IN", { hour: "2-digit", minute: "2-digit" }));
console.log(d.toLocaleDateString("hi-IN", { dateStyle: "full" })); // in HindiFor formatting many dates the same way, make one formatter with Intl.DateTimeFormat and reuse it:
const fmt = new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "short", year: "numeric" });
const dates = [new Date(2026, 0, 26), new Date(2026, 7, 15), new Date(2026, 9, 2)];
console.log(dates.map((d) => fmt.format(d)).join(" | "));Comparing dates and measuring the gap
Dates compare as their millisecond numbers, so < and > work, and subtracting two dates gives the difference in milliseconds. === does not work: two Date objects are never the same object:
const a = new Date(2026, 9, 31);
const b = new Date(2026, 11, 25);
console.log(a < b, b - a); // b - a is in milliseconds
console.log(a === new Date(2026, 9, 31)); // false: different objects
console.log(a.getTime() === new Date(2026, 9, 31).getTime()); // compare the numbers
const DAY = 24 * 60 * 60 * 1000; // milliseconds in a day
console.log("days between:", Math.round((b - a) / DAY));Math.round (rather than floor) protects against days that are 23 or 25 hours long when clocks change for daylight saving.
Adding days and months
The set... methods change a date in place, and they roll over sensibly: day 32 of October becomes 1 November. That makes adding days easy:
function addDays(date, n) {
const copy = new Date(date); // don't change the original
copy.setDate(copy.getDate() + n);
return copy;
}
const start = new Date(2026, 9, 25);
console.log(addDays(start, 10).toDateString()); // rolls into November
console.log(addDays(start, -30).toDateString()); // and back into September
const due = new Date(2026, 0, 31);
due.setMonth(due.getMonth() + 1); // 31 January + 1 month...
console.log(due.toDateString()); // ...is 3 March: February has no 31stThe last example is a classic trap: adding a month to the 31st overflows into the following month. Clamp to the month's last day if that matters.
Checking that a date is valid
An invalid date doesn't throw; it becomes Invalid Date, whose time is NaN. Impossible days are trickier: new Date(2026, 1, 30) quietly rolls over to 2 March, and browsers disagree about text like "2026-02-30". To check a date someone typed, build it from its parts and see whether the parts survived:
const bad = new Date("not a date");
console.log(String(bad), Number.isNaN(bad.getTime()));
function isRealDate(year, month, day) { // month 1 to 12, as people write it
const d = new Date(year, month - 1, day);
return d.getFullYear() === year && d.getMonth() === month - 1 && d.getDate() === day;
}
console.log(isRealDate(2026, 2, 28), isRealDate(2026, 2, 30), isRealDate(2028, 2, 29));Time zones and UTC
A Date is one moment, but people around the world see different local times for it. Methods without "UTC" in their name (and toLocale... without a timeZone option) use the visitor's time zone. To show a time in a specific zone, pass timeZone:
const launch = new Date(Date.UTC(2026, 9, 31, 12, 0)); // 12:00 UTC
const opts = { hour: "2-digit", minute: "2-digit", timeZoneName: "short" };
console.log(launch.toLocaleTimeString("en-IN", { ...opts, timeZone: "Asia/Kolkata" }));
console.log(launch.toLocaleTimeString("en-GB", { ...opts, timeZone: "Europe/London" }));
console.log(launch.toLocaleTimeString("en-US", { ...opts, timeZone: "America/New_York" }));Store and send dates as UTC (toISOString() or a timestamp), and convert to local time only when you show them.
Relative times: "3 days ago"
Intl.RelativeTimeFormat writes phrases like "in 2 days" or "yesterday":
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
console.log(rtf.format(-1, "day"), "|", rtf.format(2, "day"), "|", rtf.format(-3, "hour"), "|", rtf.format(1, "week"));
function ago(date, now) {
const seconds = Math.round((date - now) / 1000);
const units = [["year", 31536000], ["month", 2592000], ["day", 86400], ["hour", 3600], ["minute", 60]];
for (const [unit, size] of units) {
if (Math.abs(seconds) >= size) return rtf.format(Math.round(seconds / size), unit);
}
return rtf.format(seconds, "second");
}
const now = new Date(2026, 9, 31, 12, 0);
console.log(ago(new Date(2026, 9, 31, 11, 55), now), "|", ago(new Date(2026, 9, 28), now));Classic date programs
// age on a given day
function age(birth, on) {
let years = on.getFullYear() - birth.getFullYear();
const hadBirthday = on.getMonth() > birth.getMonth() ||
(on.getMonth() === birth.getMonth() && on.getDate() >= birth.getDate());
return hadBirthday ? years : years - 1;
}
console.log(age(new Date(2007, 7, 15), new Date(2026, 9, 31)));
// how many days in a month: day 0 of the NEXT month is the last day of this one
const daysInMonth = (year, month) => new Date(year, month + 1, 0).getDate();
console.log(daysInMonth(2026, 1), daysInMonth(2024, 1), daysInMonth(2026, 9));
// is it a weekend?
const isWeekend = (d) => d.getDay() === 0 || d.getDay() === 6;
console.log(isWeekend(new Date(2026, 9, 31)), isWeekend(new Date(2026, 10, 2)));
// days until an exam
const DAY = 86400000;
console.log(Math.ceil((new Date(2026, 11, 1) - new Date(2026, 9, 31)) / DAY), "days to go");new Date(year, month + 1, 0) asks for "day 0" of the next month, which rolls back to the last day of this month: a neat trick for days-in-a-month.
Right now
These use the current moment, so your output is different every time:
const now = new Date();
console.log(now.toLocaleString("en-IN"));
console.log("Today is", now.toLocaleDateString("en-IN", { weekday: "long" }));
console.log("Milliseconds since 1970:", Date.now());Key points
- A Date is a moment: milliseconds since 1 Jan 1970 UTC;
Date.now()is the current one. - Months run 0 to 11 and
getDay()0 (Sunday) to 6; days of the month start at 1. - Only ISO text (
YYYY-MM-DD) parses reliably; a date-only string means midnight UTC. - Format with
toLocaleDateString(locale, options)orIntl.DateTimeFormat; pass the locale for consistent results. - Subtract dates for milliseconds; divide by 86,400,000 for days; compare with
getTime(), not===. setDate(getDate() + n)adds days and rolls over months and years.
Practice
- Print today's date as
DD-MM-YYYY. - Write
daysUntil(dateText)that returns how many days remain until a date like"2026-12-25". - Print the day of the week you were born on.
- Print every date in February 2028 that falls on a Sunday.
- Show the current time in Tokyo, London and New York.