The Date object: one number, many getters
- Explain a
Dateas one number (milliseconds since 1970-01-01T00:00Z) and readtoISOString()as that instant printed in UTC - Build dates with the constructor forms, remembering that months are 0-indexed (
new Date(2026, 0, 31)is January 31) - Tell
getDate(day of the month) fromgetDay(day of the week, 0–6) when reading a date's parts - Predict how setters mutate in place and overflow:
setMonthon January 31 lands in March,setDate(0)is the previous month's last day - Detect an Invalid Date with
Number.isNaN(d.getTime()), and know where it fails quietly (JSON.stringifywritesnull) and loudly (toISOStringthrows)
Read
A Date is one number
FlagA Date doesn't store a year, a month and a day. It stores one number: the milliseconds
since 1970-01-01 00:00:00 UTC, called the epoch. The year, the weekday and the string you print
are all computed from that number when you ask for them.
const epoch = new Date(0);
epoch.getTime(); // 0
epoch.toISOString(); // "1970-01-01T00:00:00.000Z"
Date.now(); // e.g. 1773154800000: just the number, no object
new Date(); // the same instant, wrapped in a Date
Because there's a number underneath:
b - ais elapsed milliseconds. Subtraction converts both Dates to their numbers.a < bworks,a === bdoesn't.<and>compare the numbers, but===compares the objects, so two Dates for the same instant are not===. Comparea.getTime() === b.getTime().- Copying is
new Date(d)(ornew Date(d.getTime())): a second, independent object holding the same instant.
toISOString() prints that instant in UTC. The trailing Z means "zero offset", so it prints
the same string on every machine. The get… methods (getHours, getDate, …) read the same
number through the machine's time zone. That difference is the next unit; this one stays on one
machine in one zone.
Setters change the Date in place, and overflow
FlagEvery set… method changes the Date it's called on and returns the new timestamp (a
number), not a new Date. Two habits from other APIs break here:
const due = new Date(2026, 2, 10);
const nextWeek = due.setDate(due.getDate() + 7);
// nextWeek is a NUMBER (the new timestamp), not a Date
// due itself is now March 17: the "original" moved
const start = new Date(2026, 2, 10);
const end = start; // the same object, not a copy
end.setDate(end.getDate() + 7);
start.getDate(); // 17: start moved too
Copy first, then mutate the copy: const end = new Date(start); end.setDate(...).
Out-of-range values roll over
A setter (or the constructor) given a value outside its range carries the excess into the next
field, the way 65 minutes becomes an hour and 5 minutes:
const d = new Date(2026, 2, 31); d.setDate(32); // April 1
const e = new Date(2026, 2, 10); e.setDate(0); // February 28: day 0 is the day before the 1st
const f = new Date(2026, 5, 15); f.setMonth(12); // January 15, 2027
That makes "add n days" easy: d.setDate(d.getDate() + n) crosses month and year ends for you.
It also makes "add a month" wrong at the end of a month. setMonth keeps the day number, and
February 31 doesn't exist, so it rolls on:
const renews = new Date(2026, 0, 31);
renews.setMonth(renews.getMonth() + 1); // March 3, 2026 (Feb 31 = Feb 28 + 3 days)
// In 2028, a leap year, the same code lands on March 2.
If your rule is "same day next month, or the last day of a shorter month", write that rule down in code:
function addMonthsClamped(date, n) {
const d = new Date(date); // never mutate the caller's Date
const day = d.getDate();
d.setDate(1); // park on the 1st so changing the month can't overflow
d.setMonth(d.getMonth() + n);
const lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();
d.setDate(Math.min(day, lastDay));
return d;
}
addMonthsClamped(new Date(2026, 0, 31), 1); // February 28, 2026
addMonthsClamped(new Date(2028, 0, 31), 1); // February 29, 2028
new Date(year, month + 1, 0) is the idiom for "last day of month": day 0 of the next month.
Invalid Date is a Date, and it fails quietly
FlagWhen the constructor can't make an instant out of a string or a number, it doesn't throw. It
returns a Date whose number is NaN, printed as "Invalid Date". (Only a value that can't be
converted to a number at all throws: new Date(1n) and new Date(Symbol()) are TypeErrors.)
new Date("next tuesday"); // Invalid Date (unparseable)
new Date("2026-13-01"); // Invalid Date (month 13)
new Date(undefined); // Invalid Date (undefined → NaN)
new Date(null); // 1970-01-01T00:00:00.000Z (null → 0, NOT invalid)
const d = new Date(2026, 2, 10);
d.setMonth(undefined); // returns NaN, and d is now Invalid Date
The problem is how it surfaces, which is mostly not at all:
| You do | With an Invalid Date |
|---|---|
d instanceof Date | true |
d.getTime(), d.getFullYear() | NaN |
String(d) | "Invalid Date" |
JSON.stringify({ d }) | '{"d":null}': the API receives null |
d < now, d > now | both false: filters drop the row silently |
list.sort((a, b) => a - b) | the comparator returns NaN, treated as "equal", so the order around it is unreliable |
d.toISOString() | throws RangeError: Invalid time value |
So the one loud failure is often far from the cause: a report that crashes on toISOString()
long after the bad string came in, or a payload with "due": null that nobody notices.
Check at the boundary, where the input arrives, and fail there:
function parseInstant(input) {
const d = new Date(input);
if (Number.isNaN(d.getTime())) throw new RangeError(`not a date: ${input}`);
return d;
}
Number.isNaN(d.getTime()) says exactly what it checks. The global isNaN(d) also works,
because it converts the Date to its number first, but it reads like a mistake. instanceof Date
and truthiness checks pass an Invalid Date straight through.
Quiz
What does a Date object actually hold?
Which day is new Date(2026, 1, 14)?
A task due Tuesday, April 14, 2026 renders as "Due on the 2nd". Which call is in the template?
Flaga and b were each built with new Date(2026, 2, 10). Which expression is true?
On a laptop in New York (UTC−5 in January), what does new Date(2026, 0, 1).toISOString() print?
What's wrong with const nextWeek = due.setDate(due.getDate() + 7);?
The rule is "same day next month, or the last day of a shorter month" (January 31 → February 28, 2026). Which code does that for every start date?
Flagconst d = new Date(input); Which check rejects an Invalid Date?
Fill in the blank
Build December 25, 2026, at local midnight.
Flagconst christmas = new Date(2026, , 25);
Day 0 of a month is the last day of the month before. Get the number of days in February 2028.
Flagconst daysInFeb2028 = new Date(2028, , ).getDate(); // 29
Add a week without moving the caller's start.
const end = new Date(start.()); end.(end.getDate() + 7);
Fail at the boundary instead of passing an Invalid Date along.
Flagconst due = new Date(input); if (Number.(due.())) throw new RangeError(`not a date: ${input}`);
Predict the output
const d = new Date(2026, 0, 31); // January 31, 2026 d.setMonth(d.getMonth() + 1); // "one month later" console.log(d.getMonth() + 1, d.getDate());
const start = new Date(2026, 2, 10); // March 10 const next = start.setDate(start.getDate() + 1); console.log(typeof next, start.getDate());
const due = new Date("2026-13-01");
console.log(due instanceof Date, due.getTime(), JSON.stringify({ due }));