Topic 4.6
Dates and Times
In one line
A Date stores a single moment as the number of milliseconds since 1 January 1970 UTC (a timestamp), and shows it either on the computer's local clock or in UTC. Months count from 0, setters change the object, parsing only works reliably for ISO strings, and the new Temporal API fixes most of these old problems.
Think of it like this
A stopwatch started at midnight on 1 January 1970 in London, which has been ticking ever since. Any moment in history is simply "how many milliseconds the stopwatch showed then". People in Delhi and New York read that same moment off their own wall clocks, so they say different hours, but the stopwatch number is the same for everyone. A JavaScript Date is that stopwatch reading; the wall clock is how you choose to display it.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Timestamp
- A moment written as one number: milliseconds since 1 January 1970 UTC.
- Unix epoch
- The starting point of timestamps: midnight UTC on 1 January 1970.
- ISO 8601
- The international date format, like 2024-01-15T10:30:00Z: biggest unit first.
- Local time
- The time on the clock of the computer running the code, in its time zone.
- Time zone offset
- How far a zone's clock is from UTC, like +5:30 for India.
- Daylight saving time (DST)
- Moving clocks forward an hour in summer in some countries, so some days have 23 or 25 hours.
- Mutable
- Can be changed after it's made. A
Date's setters change the object itself. - Invalid Date
- A
Datemade from text that couldn't be understood. Its timestamp isNaN. - Temporal
- The new built-in date and time API that replaces
Date's confusing parts.
Step by step
01A Date is a number
Under the hood every Date is a timestamp. You can always get it back with getTime(), and you can make a Date from it.
const d = new Date("2024-01-15T10:30:00Z");
console.log(d.getTime()); // 1705314600000
console.log(new Date(0).toISOString()); // 1970-01-01T00:00:00.000Z02Read the parts, remembering month 0
Use UTC getters to read the parts the same way on every computer. Add 1 to the month for humans.
console.log(d.getUTCFullYear()); // 2024
console.log(d.getUTCMonth()); // 0 (January!)
console.log(d.getUTCDate()); // 15
console.log(d.getUTCDay()); // 1 (Monday; Sunday is 0)03Add days on a copy
Copy the date so the original stays the same, then let the setter roll over the month for you.
const due = new Date(d); // a copy
due.setUTCDate(due.getUTCDate() + 20); // 15 Jan + 20 days
console.log(due.toISOString()); // 2024-02-04T10:30:00.000Z
console.log(d.toISOString()); // unchanged04Measure the gap
Subtracting dates gives milliseconds. Divide by the milliseconds in a day. Working in UTC avoids DST days that are an hour short or long.
const DAY = 24 * 60 * 60 * 1000;
console.log((due - d) / DAY); // 2005Store in UTC, show in local
Send toISOString() to your server and database. When showing it to a person, format it in their zone with Intl (Topic 4.5). The same rule applies in back-end systems: Hectal's /system-design course stores event times in UTC for the same reason.
const payload = JSON.stringify({ placedAt: d }); // {"placedAt":"2024-01-15T10:30:00.000Z"}
const back = new Date(JSON.parse(payload).placedAt);
console.log(back.toLocaleString("en-IN", { timeZone: "Asia/Kolkata" }));Try it yourself
- 1
Your birthday's weekday
In the first example, change
dto your birthday at midnight UTC, likenew Date("2010-06-21T00:00:00Z"). Predict the numbergetUTCDay()prints, then check thelabel. - 2
Find month lengths
In the second example, use the day-0 trick to print how many days February has in 2100. Is 2100 a leap year? (Centuries are leap years only when divisible by 400.)
- 3
Add a month safely
In the second example, write a function
addMonthsClamped(date, n)that adds months but keeps 31 January + 1 month on the last day of February. Hint: compute the last day of the target month with the day-0 trick and take the smaller day.
Code & diagrams
`fromParts === d` is `false`: they are two different objects holding the same moment. `"15/01/2024"` isn't ISO, and Chrome can't read it. `new Date(2024, 0, 1)` uses local time, but its month is `0` everywhere.
Preview
Console
Nothing logged yet.Expected console output
1705314600000 2024-01-15T10:30:00.000Z
2024 0 15 1 10
true false
1970-01-01T00:00:00.000Z 0
2024-01-15T00:00:00.000Z
Mon 15 Jan 2024
Invalid Date NaN true
number true`alias` is the same object as `start`, so changing one changed both; `copy` stayed. 2024 is a leap year, so February 2024 has 29 days and "31 February" rolls over to 2 March.
Preview
Console
Nothing logged yet.Expected console output
2024-01-15T00:00:00.000Z 2024-01-15T10:30:00.000Z
2024-02-01T00:00:00.000Z
2024-03-02T00:00:00.000Z
29 28
46 true
[ '2024-01-15', '2024-02-01', '2024-03-01' ]
{"at":"2024-03-01T00:00:00.000Z"}Not a live example: support is still arriving across browsers. The ideas carry over: separate types for moments, calendar dates and durations, and no mutation.
// Temporal objects never change; every method returns a new one.
const today = Temporal.PlainDate.from("2024-01-31");
console.log(today.month); // 1 (January is 1, at last)
console.log(today.add({ months: 1 }).toString()); // "2024-02-29" (clamped, not rolled over)
const meeting = Temporal.ZonedDateTime.from("2024-01-15T16:00[Asia/Kolkata]");
console.log(meeting.withTimeZone("America/New_York").toString());
// "2024-01-15T05:30:00-05:00[America/New_York]"
const gap = Temporal.PlainDate.from("2024-01-15").until("2024-03-01");
console.log(gap.days); // 46
// Feature-detect before use, or load the official polyfill
if (typeof Temporal === "undefined") console.log("Temporal not available here");Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use the month number a person would
Build 15 March 2024 with new Date(Date.UTC(2024, 3, 15)).
Break #2
Change a shared date
Write function addDay(d) { d.setDate(d.getDate() + 1); return d; } and call addDay(order.date) to show tomorrow's delivery.
Break #3
Parse a local-looking string
Parse a form value with new Date("01/02/2024").
Myth vs fact
Myth
A Date stores a time zone.
Fact
It stores only a UTC timestamp. "Local" methods convert using the time zone of the computer running the code at that moment.
Myth
new Date("2024-01-15") is midnight where I am.
Fact
Date-only ISO strings are read as UTC midnight. In India that's 5:30 am; in New York it's still the 14th. Date-time strings without Z are read as local.
Myth
A day is always 86,400,000 milliseconds.
Fact
In UTC, yes (leap seconds are ignored). In local time zones with daylight saving, one day a year is 23 hours and another is 25, so local arithmetic can be off by an hour.
Myth
You need a library like Moment.js for dates.
Fact
Moment.js is in maintenance mode. Intl.DateTimeFormat handles display, careful Date code handles most maths, and Temporal replaces the rest as support arrives.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
A
Datecan hold ±8.64 × 10^15 ms, about 273,790 years either side of 1970. Timestamps use ms in JavaScript but seconds in Unix tools and many APIs: multiply or divide by 1000 at the boundary, or you'll land in 1970. - ▸
Browsers deliberately reduce the precision of
Date.now()andperformance.now()(and add jitter) to block timing attacks such as Spectre. For measuring durations, useperformance.now(), which is monotonic: it never jumps when the system clock is adjusted, unlikeDate.now(). - ▸
Legacy quirks to know:
getYear()returns years since 1900 (deprecated); two-digit years in the parts form mean 1900s (new Date(99, 0)is 1999);Date()called withoutnewreturns a string, not aDate. - ▸
Temporal reached TC39 stage 3 in 2021 and has been shipping in browsers since 2025. It uses the IANA time zone database, supports non-Gregorian calendars, and serialises with an extended ISO format that includes the zone in brackets, like
2024-01-15T16:00+05:30[Asia/Kolkata].
Remember this
- 1
A **
Dateobject holds one number: the timestamp, the milliseconds since the Unix epoch** (1970-01-01T00:00:00Z; theZmeans UTC).Date.now()returns the current timestamp, anddate.getTime()returns a date's. Because it's one number, aDateis the same moment everywhere on Earth. It doesn't store a time zone. It can only show that moment in two ways: in UTC (the world reference clock) or in the local time zone of the computer running the code. - 2
Making dates.
new Date()is now.new Date(1705314600000)is a timestamp.new Date("2024-01-15T10:30:00Z")parses an ISO 8601 string, the international formatYYYY-MM-DDTHH:mm:ss.sssZ; this is the only string format the spec guarantees, so don't parse"15/01/2024"or"Jan 15 2024"(browsers disagree). A trap: an ISO date-only string ("2024-01-15") is read as UTC midnight, but a date **and time withoutZ** ("2024-01-15T10:30") is read as local time.new Date(2024, 0, 15)builds a local date from parts, andDate.UTC(2024, 0, 15)returns a UTC timestamp from parts. - 3
Months count from 0. In the number-parts form and in
getMonth(), January is0and December is11, while days of the month start at1. This came from Java's 1995Dateclass and can never be changed.getDay()returns the weekday, with Sunday as0;getDate()is the day of the month. Every getter has a local version (getFullYear,getMonth,getDate,getHours...) and a UTC version (getUTCFullYear,getUTCMonth...). Use the UTC ones when the answer must not depend on where the code runs. - 4
Changing and calculating. Setters such as
setDate,setMonthandsetUTCHourschange the object itself (unlike strings, dates are mutable), so copy first withnew Date(date)if others use it. Setters roll over: day 32 of January becomes 1 February, which makes "add 10 days" easy (d.setUTCDate(d.getUTCDate() + 10)) but makes "add a month" to 31 January land in March. Subtracting two dates gives milliseconds (b - a); divide by1000 * 60 * 60 * 24for days, and do it on UTC dates, because a local day can be 23 or 25 hours long when clocks change for daylight saving time (DST). - 5
Showing and sending dates.
toISOString()always gives the UTC ISO string (2024-01-15T10:30:00.000Z), andJSON.stringifyuses it. Store and send moments as ISO strings in UTC or as timestamps; display them withIntl.DateTimeFormatortoLocaleStringin the user's zone (Topic 4.5).getTimezoneOffset()returns the local offset in minutes with the sign flipped: India (UTC+5:30) gives-330. An impossible date gives an Invalid Date, whosegetTime()isNaN; check withNumber.isNaN(d.getTime()). Comparing two dates with===compares objects, not moments, so comparegetTime()values (or use<and>, which convert to numbers). - 6
**
Temporal** is the modern replacement, designed by TC39 to fixDate: its objects are immutable, months start at 1, it has separate types for different jobs (Temporal.Instantfor an exact moment,Temporal.PlainDatefor a calendar date with no time or zone such as a birthday,Temporal.ZonedDateTimefor a moment in a named zone,Temporal.Durationfor lengths of time) and it understands every time zone and calendar. Firefox and Chrome have started shipping it; check support (or use the official polyfill) before relying on it. Until it's everywhere, useDatecarefully, as this topic shows.
Explain it without notes
What does a Date object actually store?
Why do months start at 0, and where does that bite?
How should dates be stored in a database or sent in an API, and how should they be displayed?
How do you safely add days to a date and count the days between two dates?
Practice
Write daysBetween(a, b) that takes two ISO date strings like "2024-01-15" and returns the whole number of days between them.
Write isWeekend(isoString) that returns true for Saturdays and Sundays, judged in UTC.
Write ageOn(birthIso, onIso) that returns a person's age in whole years on a given date.
Trade-offs
- ↔
Working in UTC everywhere makes code predictable and portable, but users think in local time, so you must convert at the edges (input and display) every time.
- ↔
Plain
Dateneeds no download and works everywhere but is mutable and easy to misuse;Temporal(or a polyfill) is clearer and safer, at the cost of newer browser support or extra bundle size.
Done when you can
I can explain that a
Dateis a timestamp in milliseconds since 1970 UTC.I create dates from ISO strings or
Date.UTC, and I remember months start at 0.I use UTC getters when the result must not depend on the computer's zone.
I copy a date before changing it, and I know setters roll over.
I can count days between dates and compare dates by their timestamps.
I store dates as UTC ISO strings and format them for people with
Intl.