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:
monthis zero-indexed:0is January,11is December. This is the single most common source of off-by-one bugs.- Only
yearandmonthare required; omitted trailing arguments default to their minimum (daydefaults to1, everything else to0). - Out-of-range values roll over automatically —
new Date(2024, 12, 1)becomes January 1, 2025, andnew Date(2024, 1, 30)becomes March 1, 2024 (since February 2024 only has 29 days). - Calling
Date()withoutnewreturns a string, not aDateobject — always usenew.
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
Dateholds 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 whydate1 - date2works 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 ofNaN. Every getter on it returnsNaN, andisNaN(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 ontoString()output for storage, since its format is locale- and engine-dependent. - Prefer
Date.UTC()or thegetUTC*/setUTC*methods when doing calendar math you want to be timezone-independent, such as computing the number of days between two dates. - Treat
Dateobjects 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 ofnew Date().getTime()when you just need the current timestamp — it avoids an unnecessary object allocation. - Use
Intl.DateTimeFormatortoLocaleDateString()`/`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
TemporalAPI rather than hand-rolling arithmetic with the legacyDateobject.
Practice Exercises
- Write a function
isLeapYear(year)that returnstrueorfalseusing aDatetrick: constructnew Date(year, 1, 29)and check whethergetMonth()is still1(February) or has rolled over to2(March). - Write a function
addBusinessDays(date, count)that returns a newDate,countweekdays afterdate, 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
Dateobject 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 andtoISOString()are timezone-independent. - The month argument/return value is always zero-indexed (0 = January, 11 = December).
Dateobjects 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 withisNaN(date.getTime())when parsing untrusted input.
