JavaScript Date Object

The Date object is JavaScript’s built-in way of representing a single moment in time. Under the hood, every Date is just a wrapper around a number: the count of milliseconds elapsed since the Unix epoch (midnight, January 1, 1970, UTC). Once you understand that one fact, almost everything else about Date — comparisons, arithmetic, formatting, timezones — falls into place.

Dates matter constantly in real applications: timestamps on database records, countdowns, scheduling, logging, and formatting content for users in different timezones. JavaScript’s Date API is old (it predates ES5) and has some quirky corners, so this lesson covers not just the happy path but the traps that catch even experienced developers.

Overview / How it works

Internally, a Date instance stores a single numeric value called the time value: the number of milliseconds since 1970-01-01T00:00:00.000Z, positive for moments after the epoch and negative for moments before it. This value is a double-precision number, and the specification limits it to roughly ±8,640,000,000,000,000 milliseconds — about 273,790 years in either direction from the epoch. If you ever construct a date outside that range, or from unparseable input, the time value becomes NaN and the date is called an Invalid Date.

Because the time value is always UTC-based, a Date object does not “contain” a timezone. What changes between environments is only how the date is displayed: methods like getHours() or toString() interpret that UTC time value using the JavaScript engine’s local timezone setting (from the operating system, or process.env.TZ in Node.js), while the getUTC* family and toISOString() always show the same UTC-based answer no matter where the code runs. This is the single most important mental model to keep: one absolute instant in time, two different lenses (local vs. UTC) for reading it.

Date objects are also mutable. Unlike a string or number, calling a setter method like setDate() changes the same object in place rather than returning a new one. This is different from most modern date libraries (and from Temporal, the newer immutable proposal), and it’s a common source of bugs when a date is shared between parts of a program.

Syntax

The Date constructor accepts several different argument shapes:

Form Example Meaning
new Date() new Date() Current date and time
new Date(milliseconds) new Date(0) Epoch time plus this many milliseconds (UTC)
new Date(dateString) new Date('2024-03-15T10:30:00.000Z') Parses an ISO 8601 (or other recognized) string
new Date(year, month, day, hours, minutes, seconds, ms) new Date(2024, 2, 15) Components in the local timezone; month is 0-indexed

Key points about the component form:

  • month is zero-indexed: 0 is January, 11 is December. This is the single most common source of off-by-one bugs.
  • Only year and month are required; omitted trailing arguments default to their minimum (day defaults to 1, everything else to 0).
  • Out-of-range values roll over automatically — new Date(2024, 12, 1) becomes January 1, 2025, and new Date(2024, 1, 30) becomes March 1, 2024 (since February 2024 only has 29 days).
  • Calling Date() without new returns a string, not a Date object — always use new.

Examples

Example 1: Creating dates in different ways

const now = new Date();
console.log(typeof now);

const specificDate = new Date('2024-03-15T10:30:00.000Z');
console.log(specificDate.toISOString());

const fromParts = new Date(2024, 2, 15, 10, 30, 0); // month 2 = March
console.log(fromParts.getFullYear(), fromParts.getMonth(), fromParts.getDate());

const fromEpoch = new Date(0);
console.log(fromEpoch.toISOString());

Output:

object
2024-03-15T10:30:00.000Z
2024 2 15
1970-01-01T00:00:00.000Z

Notice that typeof a Date is 'object', not a special 'date' type — you detect dates with instanceof Date instead. The component constructor uses whatever values you pass back exactly, since getFullYear(), getMonth(), and getDate() read the same local fields that were used to build the object.

Example 2: Reading and modifying a date

const event = new Date('2024-12-25T18:00:00.000Z');

console.log(event.getUTCFullYear());
console.log(event.getUTCMonth());
console.log(event.getUTCDate());
console.log(event.getUTCDay());
console.log(event.getUTCHours());

event.setUTCFullYear(2025);
console.log(event.getUTCFullYear());

event.setUTCMonth(event.getUTCMonth() + 2);
console.log(event.getUTCMonth());

console.log(event.toISOString());

Output:

2024
11
25
3
18
2025
1
2026-02-25T18:00:00.000Z

This example shows the mutability of Date: each set* call changes event in place and returns the new time value in milliseconds (which we ignore here). Note how setUTCMonth(13) (month index 11 + 2) automatically rolls over into the next year, turning December 2025 into February 2026 — the engine normalizes overflowing fields rather than throwing an error.

Example 3: Date arithmetic and locale formatting

function daysBetween(date1, date2) {
  const MS_PER_DAY = 1000 * 60 * 60 * 24;
  const utc1 = Date.UTC(date1.getFullYear(), date1.getMonth(), date1.getDate());
  const utc2 = Date.UTC(date2.getFullYear(), date2.getMonth(), date2.getDate());
  return Math.floor((utc2 - utc1) / MS_PER_DAY);
}

const start = new Date(2024, 0, 1);
const end = new Date(2024, 11, 31);
console.log(daysBetween(start, end));

const birthday = new Date(2024, 6, 4);
console.log(birthday.toLocaleDateString('en-US'));

const deadline = Date.now() + 1000 * 60 * 60 * 24 * 7;
console.log(typeof deadline);

Output:

365
7/4/2024
number

Because subtracting two Date objects (or their getTime() values) just subtracts numbers, date arithmetic is really millisecond arithmetic. The daysBetween helper normalizes both dates to UTC midnight first so that daylight-saving shifts in local time can’t throw off the day count. Date.now() is a static method that skips object creation entirely and returns the current epoch milliseconds directly — the fastest way to get “now” as a number.

Under the hood

  • Every Date holds one internal slot: a time value in milliseconds since the epoch, stored as a IEEE-754 double.
  • Parsing a string (new Date(str)) funnels through the engine’s date parser. ISO 8601 strings ('2024-03-15', '2024-03-15T10:30:00Z') are guaranteed to parse consistently across engines; other formats (like 'March 15, 2024') are implementation-defined and can behave differently between browsers and Node versions.
  • Component-based construction (new Date(year, month, ...)) and the local getters/setters (getHours, setDate, etc.) all go through the host environment’s timezone database to convert between the stored UTC time value and local wall-clock fields.
  • getTime(), valueOf(), and the unary + operator (+new Date()) all return the same raw millisecond number, which is why date1 - date2 works even though subtraction isn’t defined on objects — JavaScript coerces both to numbers first.
  • An invalid date (e.g. new Date('not a date')) has a time value of NaN. Every getter on it returns NaN, and isNaN(date.getTime()) is the standard way to check validity.

Common Mistakes

Mistake 1: Forgetting that months are zero-indexed

It’s easy to assume the month argument matches the calendar month number, but 0 means January.

const wrong = new Date(2024, 4, 15); // intending April 15th
console.log(wrong.getMonth()); // this is actually May!

Output:

4

The fix is to use the correct zero-based index — April is 3, not 4:

const correct = new Date(2024, 3, 15); // April is month index 3
console.log(correct.getMonth());

Output:

3

Mistake 2: Comparing dates with == or ===

Two different Date objects representing the exact same instant are still two different object references, so equality operators compare identity, not time.

const d1 = new Date(2024, 0, 1);
const d2 = new Date(2024, 0, 1);
console.log(d1 == d2);

Output:

false

To compare the actual moments in time, compare their numeric values instead:

const d1 = new Date(2024, 0, 1);
const d2 = new Date(2024, 0, 1);
console.log(d1.getTime() === d2.getTime());

Output:

true

A related trap is passing a Date to a function and having that function call a setter on it — since dates are mutable objects passed by reference, the caller’s original date silently changes too.

Best Practices

  • Store and transmit dates as ISO 8601 strings (toISOString()) or raw epoch milliseconds (getTime()) — never rely on toString() output for storage, since its format is locale- and engine-dependent.
  • Prefer Date.UTC() or the getUTC*/setUTC* methods when doing calendar math you want to be timezone-independent, such as computing the number of days between two dates.
  • Treat Date objects as if they were immutable in your own code: create a new instance (new Date(original.getTime())) before mutating, rather than calling setters on a date someone else might still hold a reference to.
  • Use Date.now() instead of new Date().getTime() when you just need the current timestamp — it avoids an unnecessary object allocation.
  • Use Intl.DateTimeFormat or toLocaleDateString()`/`toLocaleString() with an explicit locale argument for user-facing formatting, rather than hand-building date strings.
  • Always validate parsed dates with isNaN(date.getTime()) before using them, especially when parsing user input or third-party data.
  • For heavy date manipulation (recurring events, timezone conversions across DST boundaries), reach for a well-tested library or the newer Temporal API rather than hand-rolling arithmetic with the legacy Date object.

Practice Exercises

  • Write a function isLeapYear(year) that returns true or false using a Date trick: construct new Date(year, 1, 29) and check whether getMonth() is still 1 (February) or has rolled over to 2 (March).
  • Write a function addBusinessDays(date, count) that returns a new Date, count weekdays after date, skipping Saturdays and Sundays. Test it starting from a Friday.
  • Given an array of ISO date strings, write a function that filters out any invalid dates and returns the remaining dates sorted from earliest to latest.

Summary

  • A Date object wraps a single number: milliseconds since the Unix epoch (UTC), so date arithmetic is really number arithmetic.
  • Local methods (getHours, setMonth, …) read/write using the environment’s local timezone; UTC-prefixed methods and toISOString() are timezone-independent.
  • The month argument/return value is always zero-indexed (0 = January, 11 = December).
  • Date objects are mutable — setter methods change the object in place rather than returning a new one.
  • Compare dates by their numeric time value (getTime()), never with == or === directly on the objects.
  • Invalid dates have a time value of NaN; always check with isNaN(date.getTime()) when parsing untrusted input.