Command Palette

Search for a command to run...

Hectal
PHASE 4Beginner ~28 min· topic 6 of 6

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 Date made from text that couldn't be understood. Its timestamp is NaN.
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.

dates.jswhole filejavascript
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.000Z
A Date is a numberdiagram
Rendering diagram…

02Read 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.

dates.jswhole filejavascript
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.

dates.jswhole filejavascript
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());          // unchanged

04Measure 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.

dates.jswhole filejavascript
const DAY = 24 * 60 * 60 * 1000;
console.log((due - d) / DAY); // 20

05Store 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.

dates.jswhole filejavascript
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. 1

    Your birthday's weekday

    In the first example, change d to your birthday at midnight UTC, like new Date("2010-06-21T00:00:00Z"). Predict the number getUTCDay() prints, then check the label.

  2. 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. 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

Making dates and reading their parts New tab

`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

Press Run to see the page.

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
Adding time, rolling over and measuring gaps New tab

`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

Press Run to see the page.

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"}
Temporal: the modern API (check browser support first)javascript

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)).

terminal
$ Console
── what you'll see ──
2024-04-15T00:00:00.000Z (April, not March)

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.

terminal
$ the page shows
── what you'll see ──
The order date itself moved forward a day as well as the delivery date.

Break #3

Parse a local-looking string

Parse a form value with new Date("01/02/2024").

terminal
$ Console
── what you'll see ──
2 January in Chrome (read as month/day), but an Indian user meant 1 February; other formats give Invalid Date.

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 Date can 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() and performance.now() (and add jitter) to block timing attacks such as Spectre. For measuring durations, use performance.now(), which is monotonic: it never jumps when the system clock is adjusted, unlike Date.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 without new returns a string, not a Date.

  • ▸

    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. 1

    A **Date object holds one number: the timestamp, the milliseconds since the Unix epoch** (1970-01-01T00:00:00Z; the Z means UTC). Date.now() returns the current timestamp, and date.getTime() returns a date's. Because it's one number, a Date is 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. 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 format YYYY-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 without Z** ("2024-01-15T10:30") is read as local time. new Date(2024, 0, 15) builds a local date from parts, and Date.UTC(2024, 0, 15) returns a UTC timestamp from parts.

  3. 3

    Months count from 0. In the number-parts form and in getMonth(), January is 0 and December is 11, while days of the month start at 1. This came from Java's 1995 Date class and can never be changed. getDay() returns the weekday, with Sunday as 0; 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. 4

    Changing and calculating. Setters such as setDate, setMonth and setUTCHours change the object itself (unlike strings, dates are mutable), so copy first with new 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 by 1000 * 60 * 60 * 24 for 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. 5

    Showing and sending dates. toISOString() always gives the UTC ISO string (2024-01-15T10:30:00.000Z), and JSON.stringify uses it. Store and send moments as ISO strings in UTC or as timestamps; display them with Intl.DateTimeFormat or toLocaleString in 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, whose getTime() is NaN; check with Number.isNaN(d.getTime()). Comparing two dates with === compares objects, not moments, so compare getTime() values (or use < and >, which convert to numbers).

  6. 6

    **Temporal** is the modern replacement, designed by TC39 to fix Date: its objects are immutable, months start at 1, it has separate types for different jobs (Temporal.Instant for an exact moment, Temporal.PlainDate for a calendar date with no time or zone such as a birthday, Temporal.ZonedDateTime for a moment in a named zone, Temporal.Duration for 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, use Date carefully, as this topic shows.

Explain it without notes

01

What does a Date object actually store?

02

Why do months start at 0, and where does that bite?

03

How should dates be stored in a database or sent in an API, and how should they be displayed?

04

How do you safely add days to a date and count the days between two dates?

Practice

01

Write daysBetween(a, b) that takes two ISO date strings like "2024-01-15" and returns the whole number of days between them.

02

Write isWeekend(isoString) that returns true for Saturdays and Sundays, judged in UTC.

03

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 Date needs 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 Date is 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.