Dates & Time
Dates & Time

The Date object: one number, many getters

Lesson · ~20 min
After this unit you can
  • Explain a Date as one number (milliseconds since 1970-01-01T00:00Z) and read toISOString() 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) from getDay (day of the week, 0–6) when reading a date's parts
  • Predict how setters mutate in place and overflow: setMonth on 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.stringify writes null) and loudly (toISOString throws)

Read

Simple
1 / 2

A Date is one number

Flag

A 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 - a is elapsed milliseconds. Subtraction converts both Dates to their numbers.
  • a < b works, a === b doesn't. < and > compare the numbers, but === compares the objects, so two Dates for the same instant are not ===. Compare a.getTime() === b.getTime().
  • Copying is new Date(d) (or new 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.

Medium

Setters change the Date in place, and overflow

Flag

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

Complex

Invalid Date is a Date, and it fails quietly

Flag

When 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 doWith an Invalid Date
d instanceof Datetrue
d.getTime(), d.getFullYear()NaN
String(d)"Invalid Date"
JSON.stringify({ d })'{"d":null}': the API receives null
d < now, d > nowboth 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?

Flag

Which day is new Date(2026, 1, 14)?

Flag

A task due Tuesday, April 14, 2026 renders as "Due on the 2nd". Which call is in the template?

Flag

a and b were each built with new Date(2026, 2, 10). Which expression is true?

Flag

On a laptop in New York (UTC−5 in January), what does new Date(2026, 0, 1).toISOString() print?

Flag

What's wrong with const nextWeek = due.setDate(due.getDate() + 7);?

Flag

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?

Flag

const d = new Date(input); Which check rejects an Invalid Date?

Flag

Fill in the blank

Build December 25, 2026, at local midnight.

Flag
const 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.

Flag
const daysInFeb2028 = new Date(2028, , ).getDate(); // 29

Add a week without moving the caller's start.

Flag
const end = new Date(start.());
end.(end.getDate() + 7);

Fail at the boundary instead of passing an Invalid Date along.

Flag
const 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());
Flag
const start = new Date(2026, 2, 10); // March 10
const next = start.setDate(start.getDate() + 1);
console.log(typeof next, start.getDate());
Flag
const due = new Date("2026-13-01");
console.log(due instanceof Date, due.getTime(), JSON.stringify({ due }));
Flag

Further reading