Date & time
Create, compare, parse, calculate, and format dates with JavaScript's Date object while avoiding time zone and locale traps.
- 01Think in timestampsTreat Date as a wrapper around one millisecond number.
- 02Parse safelyPrefer ISO strings and know which forms are UTC or local.
- 03Calculate and format deliberatelyUse UTC methods for deterministic math and locale APIs for people.
One number, many clocks
Dates feel human: birthdays, deadlines, calendars, time zones, holidays. JavaScript’s Date is much simpler and much stranger: it stores one number, a timestamp in milliseconds since 1970-01-01T00:00:00.000Z. Everything else is a way to read or format that number.
That mental model connects lessons you already know. A timestamp is a Number. JSON.stringify turns dates into ISO strings, and JSON.parse can revive them in the JSON lesson. +date uses Object to primitive conversion to get the timestamp. Comparisons work because dates can become numbers.
Imagine one stopwatch for the whole world. It does not care whether you are in Kolkata, Mumbai, New York, or Tokyo. It just counts milliseconds. A Date object is a wrapper around that count. Time zones are the glasses you put on when you read it.
- In real life: The stopwatch started at midnight UTC on January 1, 1970
- In JavaScript: Timestamp 0
- In real life: Every tick is one millisecond
- In JavaScript: The number stored by Date
- In real life: People in different cities read the same moment on different clocks
- In JavaScript: UTC and local getters show different labels
Where the analogy stops: Real stopwatches do not know calendars. Date has calendar methods, and those methods inherit old API choices such as zero-based months.
A JavaScript Date represents one instant in time as a millisecond timestamp. Its methods either read that instant in UTC, read it in your local time zone, mutate it, or format it for storage or display.
Timestamps & Date.now()
INTERACTIVEDate.now() returns the current timestamp. Because “current” depends on when the reader loads the page, the playground below computes it only after the page mounts and labels the local results in your time zone. The same instant is shown several ways.
const now = Date.now();const instant = new Date(now);instant.toISOString();instant.toString();instant.toLocaleString();instant.toLocaleString("en-US", { timeZone: "Asia/Tokyo" });mounting...mounting...mounting...mounting...mounting...mounting...mounting...mounting...Waiting until the page mounts avoids a server/browser time mismatch.
new Date(timestamp) wraps the number. toISOString() shows the same instant in UTC. toString() and the default toLocaleString() show it through your environment’s local settings. When you need deterministic output, pass an explicit locale and timeZone to Intl.DateTimeFormat or toLocaleString options.
Step through a Date as a wrapper around one timestamp. Predict the month before line 3.
script
const moment = new Date(stamp);const month = moment.getUTCMonth();const iso = moment.toISOString();const number = +moment;The timestamp does not change when you ask for UTC or local time. Only the label changes, like two people standing in different cities describing the same video call.
- In real life: The object you are looking at
- In JavaScript: The timestamp
- In real life: UTC glasses
- In JavaScript: getUTCFullYear(), toISOString()
- In real life: Local glasses
- In JavaScript: getFullYear(), toString()
- In real life: Locale glasses
- In JavaScript: toLocaleString() and Intl
Where the analogy stops: Glasses do not change the object. Date setters do mutate the Date object, so treat setters as tools that rewrite the timestamp.
Creating & parsing dates
INTERACTIVEThere are four common ways to create dates. Use new Date() for the current instant, new Date(timestamp) when you already have milliseconds, Date.UTC(...) plus new Date(...) for deterministic components, and ISO strings for stored data.
| Form | Meaning | Watch out |
|---|---|---|
new Date() | The current instant | Client-only in rendered lessons or UI because it changes every millisecond |
new Date(1710000000000) | Wrap this timestamp | The number is UTC-based even if local methods display another clock time |
new Date(2024, 0, 31) | Local time components | Months are 0-based: 0 is January |
new Date(Date.UTC(2024, 0, 31)) | UTC components | Date.UTC returns a timestamp, not a Date object |
new Date("2024-03-10T09:00Z") | Parse an ISO instant | Include Z or an offset when the instant matters |
Parsing is where old Date hurts. ISO strings are specified; many natural-language or slash formats are implementation-dependent. Try the lab, then notice which rows say UTC, local, offset, or invalid.
new Date("2024-03-10"); // date-only ISO: UTCnew Date("2024-03-10T09:00"); // no offset: local timenew Date("2024-03-10T09:00Z"); // Z: UTCnew Date("2024-03-10T09:00+05:30");new Date("March 10, 2024"); // non-ISO: avoid for datanew Date("not a date").getTime(); // NaNThe lab waits until mount before parsing strings whose result can depend on the reader's time zone.
"2024-03-10"is a date-only ISO string, so it parses as midnight UTC."2024-03-10T09:00"has a time but no offset, so it parses as 9:00 in your time zone."2024-03-10T09:00Z"and"2024-03-10T09:00+05:30"name exact instants.- Invalid dates produce a
DatewhosegetTime()isNaN.
ISO 8601, Date.UTC & time zones
SORTISO 8601 is the family of date formats like 2024-03-10T09:00:00.000Z. The Z suffix means UTC. An offset such as +05:30 means “this local clock time in a place five and a half hours ahead of UTC.” Both define a single instant.
date.toISOString()Date.UTC(2024, 0, 31)new Date("2024-03-10")new Date("2024-03-10T09:00")date.getHours()date.setMonth(1)date.toLocaleString()Intl.DateTimeFormat("en-GB", { timeZone: "UTC" })
Sort each Date method or string by what it depends on.
Date.UTC(year, monthIndex, day, hour) is a reliable way to construct a timestamp from components. Remember the month index: January is 0, December is 11. It returns a number; wrap it with new Date(...) when you want Date methods.
Getters, setters & overflow
STEP THROUGHDates have local getters such as getFullYear(), getMonth(), getDate(), getDay(), and getHours(). They also have UTC versions: getUTCFullYear(), getUTCMonth(), and so on. The UTC versions are best for tests because they do not depend on the machine’s time zone.
const jan31 = new Date(Date.UTC(2024, 0, 31, 9));jan31.getUTCFullYear(); // 2024jan31.getUTCMonth(); // 0, Januaryjan31.getUTCDate(); // 31Setters mutate the existing Date. They also overflow rather than clamp. If you ask for “February 31,” Date rolls into March. That is sometimes useful, sometimes a bug.
Watch setters mutate the same Date object and allow overflow. UTC methods keep this replay deterministic.
script
due.setUTCMonth(due.getUTCMonth() + 1);due.setUTCDate(due.getUTCDate() + 7);console.log(due.toISOString());If two variables point at the same Date object, setMonth or setDate changes what both variables see. Copy first with new Date(oldDate.getTime()) when you need the original.
Date arithmetic
For exact instants, subtract timestamps. The result is milliseconds. Dividing by 86_400_000 gives 24-hour chunks. That is perfect for UTC timestamps like API expiry times, but it is not always the same as “calendar days in a local time zone” because daylight saving time can make local days 23 or 25 hours long.
const a = new Date("2024-06-01T00:00:00Z");const b = new Date("2024-06-01T00:00:00Z");console.log(a === b);console.log(a.getTime() === b.getTime());console.log(a < new Date("2024-06-02T00:00:00Z"));Two Date objects are objects, so === checks whether they are the same object, not whether they represent the same instant. Use getTime(), unary +date, or relational comparisons like < when you mean the timestamp.
const due = new Date(Date.UTC(2024, 0, 31, 9));due.setUTCMonth(due.getUTCMonth() + 1);due.setUTCDate(due.getUTCDate() + 7);console.log(due.toISOString());Much of this pain is why JavaScript is getting Temporal, the modern replacement covered next. Temporal separates exact instants, plain calendar dates, time zones, and durations instead of forcing one old object to do all of them.
toISOString, toJSON & toLocaleString
Use different output for machines and humans. toISOString() is for storage and APIs: always UTC, always a Z suffix. toJSON() calls toISOString(), which is why JSON turns Date objects into ISO strings. toLocaleString(), toLocaleDateString(), and Intl.DateTimeFormat are for people.
const instant = new Date("2024-12-24T18:30:00Z");console.log(instant.toISOString());console.log(instant.toJSON());console.log(new Intl.DateTimeFormat("en-GB", { weekday: "long", day: "numeric", month: "long", year: "numeric", timeZone: "UTC"}).format(instant));For UI, pass the locale and options on purpose: en-GB and en-US order dates differently; weekday and month: "long" make labels friendlier. The Internationalization with Intl lesson goes much deeper into formatting dates, numbers, lists, and plurals.
Where you’ll use this
Date work appears in forms, dashboards, calendars, cache expiration, logs, and API data. A practical workflow is: store an ISO string, revive or wrap it when you need Date methods, calculate with timestamps or UTC methods, then format at the edge for a specific audience.
| Job | Good default | Why |
|---|---|---|
| Store in JSON | ISO string from toISOString() | Stable UTC text that survives systems and time zones |
| Show to a user | Intl.DateTimeFormat(locale, options) | People expect their local language and date order |
| Compare expiry times | getTime() or < | You are comparing instants |
| Add calendar days in tests | UTC getters/setters | Avoid machine-local time zone surprises |
| Schedule future app features | Temporal | It models dates, times, zones, and durations separately |
Pitfalls & misconceptions
“Months are 1-based.”
Date component months are 0-based. new Date(2024, 0, 31) is January 31.
“All ISO-looking strings are UTC.”
Date-only ISO strings are UTC, but date-time strings without an offset are local time.
“Invalid dates throw immediately.”
Usually they do not. You get Invalid Date, and getTime() returns NaN.
“Adding one month clamps to the last day.”
Setters overflow. January 31 plus one month rolls into March, with local results affected by the year and time zone.
“toLocaleString is good for tests.”
Only when you pass explicit locale and timeZone. Otherwise it depends on the environment.
“Two matching Date objects are ===.”
They are separate objects. Compare timestamps.
| Problem | Surprise | Safer habit |
|---|---|---|
| Month numbers | January is 0 | Name variables monthIndex, or use ISO strings |
| Parsing | Non-ISO varies | Accept ISO only for data |
| Local math | DST changes hour counts | Use UTC for fixed 24-hour chunks; use Temporal for calendar math |
| Formatting | Defaults differ by reader | Pass locale and timeZone |
Practice: dates without surprises
5 EXERCISESPredict the output. This checks the old building “ground floor is 0” rule.
const birthday = new Date(2024, 0, 31);
console.log(birthday.getMonth());It prints 0. Month numbers start at zero, so January is 0, February is 1, and December is 11.
Work out the number of days without running it first.
const start = new Date("2024-06-01T00:00:00Z");
const end = new Date("2024-06-11T00:00:00Z");
console.log((end - start) / 86400000);The timestamps are exactly ten 24-hour chunks apart, so the program prints 10. This is deterministic because both strings end with Z.
Run the code and compare the two lines. Then change dateStyle to "medium".
const instant = new Date("2024-12-24T18:30:00Z");
console.log(new Intl.DateTimeFormat("en-GB", { dateStyle: "full", timeZone: "UTC" }).format(instant));
console.log(new Intl.DateTimeFormat("en-US", { dateStyle: "full", timeZone: "UTC" }).format(instant));const instant = new Date("2024-12-24T18:30:00Z");
console.log(new Intl.DateTimeFormat("en-GB", { dateStyle: "full", timeZone: "UTC" }).format(instant));
console.log(new Intl.DateTimeFormat("en-US", { dateStyle: "full", timeZone: "UTC" }).format(instant));The same instant can be formatted with different locale rules. UK English prints day before month; US English prints month before day. The Intl lesson covers these options in depth.
The code accidentally creates a date in the next year. What month number should December use?
// Bug: intended December 5, 2024
const launch = new Date(2024, 12, 5);
console.log(launch.toISOString());const launch = new Date(2024, 11, 5);Use month index 11 for December. Month index 12 overflows into January of the next year.
Why does this print true even though two separate Date objects were created?
const a = new Date("2024-06-01T00:00:00Z");
const b = new Date("2024-06-01T00:00:00Z");
console.log(a.getTime() === b.getTime());const a = new Date("2024-06-01T00:00:00Z");
const b = new Date("2024-06-01T00:00:00Z");
console.log(a.getTime() === b.getTime());Two Date objects are different objects, but their timestamps can be equal. getTime() makes the comparison about the instant.
Quiz: check your understanding
7 QUESTIONSQuestion 1 of 7What does a JavaScript Date object store internally?
Choose an answer to see the explanation.
Question 2 of 7What month number does this print?
Read the code, then predictconsole.log(new Date(Date.UTC(2024, 0, 31)).getUTCMonth());Choose an answer to see the explanation.
Question 3 of 7Which string is parsed as midnight UTC?
Choose an answer to see the explanation.
Question 4 of 7What does this invalid-date check print?
Read the code, then predictconst d = new Date("not a date"); console.log(Number.isNaN(d.getTime()));Choose an answer to see the explanation.
Question 5 of 7What type does Date.UTC return here?
Read the code, then predictconst stamp = Date.UTC(2024, 0, 1); console.log(typeof stamp);Choose an answer to see the explanation.
Question 6 of 7How should you compare two Date objects for the same instant?
Choose an answer to see the explanation.
Question 7 of 7Which formatter always uses UTC and includes a Z suffix?
Choose an answer to see the explanation.
Key takeaways
- A
Datestores one millisecond timestamp; methods choose how to read it. Date.now()andDate.UTC()return numbers.new Date(...)returns an object.- Months are 0-based in component constructors and setters.
- Date-only ISO strings parse as UTC; date-time strings without offsets parse in local time.
- Use UTC methods and explicit
timeZoneoptions for deterministic tests. toISOString()is for machines;Intl.DateTimeFormatand locale methods are for people.
Remember the one-liner.
A Date is one timestamp read through UTC, local, or locale-specific glasses.
Up next: Temporal, the modern replacement that fixes many of Date’s pain points.