Lesson 13 of 30

JavaScript Date Object

Creating a Date, and the Month Trap

A Date represents a single moment in time. Internally it stores exactly one number: the count of milliseconds since 1 January 1970 UTC, known as the Unix epoch. Years, months, formatting and time zones are all calculated from that number at the moment you ask for them. Keeping that one fact in mind explains almost everything the object does, including the parts that look strange.

There are four ways to build one. new Date() with no arguments gives the current moment. new Date(ms) builds one from an epoch number. new Date(year, monthIndex, day, hours, minutes) builds one from parts, interpreted in the local time zone. And new Date(string) parses text.

The parts form carries the most famous trap in the language: the month is counted from zero. January is 0 and December is 11 — while the day of the month is counted from 1, exactly as you would expect. So new Date(2026, 7, 15) is 15 August 2026, not 15 July. There is no defence for the inconsistency; it is simply how it is, and it is the single most common date bug there is. getMonth() has the same offset when you read it back, so remember to add 1 before displaying it.

Parsing strings is where surprises multiply. An ISO date-only string such as '2026-08-02' is parsed as midnight UTC, while a string that includes a time but no zone, such as '2026-08-02T10:30', is parsed as local time. Anything else — '02/08/2026', 'Aug 2, 2026' — is left to each browser to interpret and can differ between them. If you are parsing, use the ISO format. If you need certainty about which calendar day it is locally, build the date from parts instead.

Example
const now = new Date();
console.log(now.getTime());       // milliseconds since 1 Jan 1970 UTC

// From parts - the month is ZERO-based, the day is not
const d = new Date(2026, 7, 15);  // 15 August 2026
console.log(d.getDate());         // 15
console.log(d.getMonth());        // 7  <- not 8
console.log(d.getMonth() + 1);    // 8  <- what you actually display

// An ISO date-only string is treated as UTC
console.log(new Date('2026-08-02').toISOString());
// '2026-08-02T00:00:00.000Z'

// From an epoch number
console.log(new Date(0).toISOString());   // '1970-01-01T00:00:00.000Z'

// Unparseable text gives an Invalid Date, not an exception
const bad = new Date('not a date');
console.log(bad.getTime());               // NaN
console.log(Number.isNaN(bad.getTime())); // true - this is how you test it
Notes
  • new Date(2026, 13, 1) does not throw. It rolls over to February 2027, because the constructor normalises out-of-range parts. That is useful for arithmetic and unhelpful for validation — it means a wrong month number silently becomes a valid date.

Reading and Changing the Parts

Getter methods pull out each piece: getFullYear, getMonth, getDate, getDay, getHours, getMinutes, getSeconds and getMilliseconds. Every one of them reports in the local time zone of the machine running the code, which becomes important two sections from now.

Two of those names cause real confusion. getDate() gives the day of the month, 1 to 31. getDay() gives the day of the week, 0 for Sunday through 6 for Saturday. They sound as though they should be the other way round, and swapping them produces dates that are wrong by weeks while still looking plausible. There is also an old getYear, which you should ignore entirely — getFullYear is the correct one.

Every getter has a matching setter, and the setters handle overflow intelligently. setDate(getDate() + 30) rolls correctly into the next month, across a year boundary, and through February in a leap year, so you never have to know how many days a month has. This is by far the safest way to do date arithmetic, and the next section explains why the obvious alternative is not.

Setters change the date object in place. That matters because dates are objects and are passed by reference, exactly like arrays: adding thirty days to a date another part of your program is holding changes it there too, silently. Copy first with new Date(original) and modify the copy. This is the same discipline as copying an array before you sort it.

Example
const d = new Date(2026, 7, 15, 14, 30, 0);   // 15 Aug 2026, 2:30 pm local

console.log(d.getFullYear());   // 2026
console.log(d.getMonth());      // 7  - August, zero-based
console.log(d.getDate());       // 15 - day of the MONTH
console.log(d.getDay());        // 6  - day of the WEEK (0 = Sunday)
console.log(d.getHours());      // 14
console.log(d.getMinutes());    // 30

// Setters mutate, so copy before changing
const due = new Date(d);
due.setDate(due.getDate() + 30);
console.log(due.getMonth() + 1, due.getDate());   // 9 14  -> 14 September
console.log(d.getDate());                         // 15 - untouched

// Formatting the parts by hand when you need a specific layout
const pad = n => String(n).padStart(2, '0');
console.log(`${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()}`);
// '15/08/2026'

// Day names, since getDay() returns a number
const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
console.log(DAYS[d.getDay()]);   // 'Saturday'
Notes
  • Every getter has a UTC twin — getUTCDate, getUTCHours and so on — which reads the same moment in UTC instead of local time. Mixing local getters with UTC setters in one calculation is a reliable way to end up a day out.

Formatting a Date for Display

Do not build display dates by hand if you can avoid it. toLocaleDateString and toLocaleTimeString format a date the way a given locale expects, they are built into every browser, and they need no library and no list of month names that you have to maintain and translate.

The locale 'en-IN' produces day/month/year ordering, which is what an Indian reader expects, while 'en-US' produces month/day/year for the same moment. An options object lets you choose long or short month names, whether to include the weekday, and 12- or 24-hour time. Because the browser produces the text, it stays correct as locale conventions are updated.

toISOString() is the opposite tool. It always produces the same machine-readable format, always in UTC, always ending in Z. That is what you send to a server and what you store in a database. Never store a locale-formatted string: '02/08/2026' means 2 August in India and 8 February in the United States, and once it has been written down that way the ambiguity cannot be recovered by anyone.

The older methods toDateString() and toString() produce fixed English formats that are useful while debugging and wrong in a user interface. If you ever see 'Sat Aug 15 2026' rendered on a real page, somebody reached for the quick option and nobody checked.

Example
const d = new Date(2026, 7, 15, 14, 30);

console.log(d.toLocaleDateString('en-IN'));   // '15/8/2026'
console.log(d.toLocaleDateString('en-US'));   // '8/15/2026'

console.log(d.toLocaleDateString('en-IN', {
  weekday: 'long',
  day: 'numeric',
  month: 'long',
  year: 'numeric'
}));
// 'Saturday, 15 August 2026' - exact punctuation is the browser's choice

console.log(d.toLocaleTimeString('en-IN', {
  hour: '2-digit',
  minute: '2-digit'
}));
// '02:30 pm' or similar, depending on the locale data

// For storing and sending - always ISO, always UTC
console.log(d.toISOString());

// Debugging only
console.log(d.toDateString());   // 'Sat Aug 15 2026'
Notes
  • If you are formatting many dates in a loop, build one Intl.DateTimeFormat object above the loop and call its format method inside. Constructing the formatter is the expensive part, and toLocaleDateString effectively builds a new one on every call.

Date Arithmetic

Because a date is a number of milliseconds underneath, subtracting one date from another gives you the gap between them in milliseconds. Divide down to whatever unit you need. That single operation covers most of what anyone actually wants from dates: how long ago was this, how many days until the exam, how long did that request take.

Comparison works the same way. d1 < d2 works directly, because the comparison operators convert both objects to numbers first. d1 === d2 does not work, for the reason Lesson 5 gave — two Date objects are two different objects even when they represent the identical instant. Compare d1.getTime() === d2.getTime() instead, and the problem disappears.

Adding time is where care is needed. Adding 24 * 60 * 60 * 1000 milliseconds adds exactly twenty-four hours, which is not always the same thing as adding one day. In any region that observes daylight saving, two days each year are 23 or 25 hours long. India does not observe it, but your users may be elsewhere and your server almost certainly is. setDate(getDate() + 1) adds a calendar day correctly in every case, so prefer it.

For measuring how long a piece of code took, do not use Date at all. performance.now() returns a high-resolution timer that cannot jump backwards when the system clock is corrected or adjusted for network time, which a wall clock can. Lesson 29 uses it for benchmarking, and it is the right tool whenever you are timing rather than dating.

Example
const start = new Date(2026, 7, 1);
const exam = new Date(2026, 7, 15);

// Subtracting two dates gives milliseconds
const ms = exam - start;
console.log(ms);                               // 1209600000
console.log(ms / (1000 * 60 * 60 * 24));       // 14 days

// Comparison operators work; === does not
console.log(exam > start);                     // true
const same = new Date(2026, 7, 15);
console.log(exam === same);                    // false - two objects
console.log(exam.getTime() === same.getTime()); // true

// Adding a calendar day safely
const tomorrow = new Date(exam);
tomorrow.setDate(tomorrow.getDate() + 1);
console.log(tomorrow.getDate());               // 16

// A reusable helper
function daysBetween(a, b) {
  const MS_PER_DAY = 1000 * 60 * 60 * 24;
  return Math.round((b - a) / MS_PER_DAY);
}
console.log(daysBetween(start, exam));         // 14

// Timing code: use performance.now(), not Date
const t0 = performance.now();
for (let i = 0; i < 100000; i++) { /* work */ }
console.log(`took ${(performance.now() - t0).toFixed(2)} ms`);
Notes
  • Math.round rather than Math.floor in daysBetween is deliberate. Across a daylight-saving boundary the gap can be 13.96 or 14.04 days, and flooring that would report 13.

Time Zones, and How to Store a Date

A Date does not store a time zone. It stores a moment, and the getters translate that moment into the local zone of whichever machine is running the code. The same Date object therefore reports 14:30 to a user in Delhi and 09:00 to a user in London — and both are right, because it is the same instant seen from two places.

That is precisely why you must never store or transmit a formatted local string. Store the ISO string from toISOString(), or the epoch number from getTime(). Both are unambiguous, both sort correctly as plain text or plain numbers, and both can be turned back into a Date anywhere in the world without any extra information.

Here is the classic bug in full. A user in India picks 1 August in a date input. Your code builds a Date at local midnight. toISOString() then produces '2026-07-31T18:30:00.000Z', because IST is five and a half hours ahead of UTC. If the server keeps only the date part of that string, the record is filed under 31 July and the user swears the system is broken. When only the calendar date matters — a birthday, a holiday, an exam date — keep it as a plain 'YYYY-MM-DD' string and never convert it into a moment in time at all.

The built-in Date object is workable but genuinely awkward, which is why libraries such as Day.js and date-fns are so widely used in real projects. A newer built-in date and time API called Temporal is being standardised to replace it; support is still arriving across browsers and runtimes, so check what your targets actually implement before you depend on it.

Example
const d = new Date(2026, 7, 1);      // local midnight on 1 August

console.log(d.toISOString());        // from IST: '2026-07-31T18:30:00.000Z'
console.log(d.getTimezoneOffset());  // -330 minutes for IST - note the sign

// Store one of these, never a display string
console.log(d.getTime());            // an epoch number
console.log(d.toISOString());        // an ISO string

// Round-tripping back to a Date gives the same instant
const back = new Date('2026-07-31T18:30:00.000Z');
console.log(back.getDate());         // 1, when read in IST

// When only the calendar date matters, keep it as text
const examDate = '2026-08-15';       // no time, no zone, no ambiguity
console.log(examDate < '2026-09-01'); // true - ISO strings sort correctly
Notes
  • getTimezoneOffset() returns UTC minus local, in minutes, so a zone ahead of UTC reports a negative number. IST gives -330. The sign catches people every single time.

The Date Bugs Worth Knowing by Name

Nearly every date bug is one of a small, well-known set. Recognising them by name is the difference between fixing one in five minutes and losing an afternoon to it.

Invalid Date is not an error. A date built from text the browser could not parse is still a real object. Every one of its getters returns NaN, and NaN inside a template literal prints as the word "NaN" on your page rather than raising anything. Always check a parsed date with Number.isNaN(d.getTime()) before you use it, especially when the text came from a user or an API.

The one-day-out bug is almost always a time zone. If a date is consistently one day earlier or later than expected, the calculation is usually fine and the conversion is not. Print d.toISOString() and d.toString() next to each other; if they name different days, you have found it.

  • The month is zero-based in new Date(y, m, d) and in getMonth() — add 1 to display
  • getDate() is the day of the month; getDay() is the day of the week
  • '2026-08-02' parses as UTC; '2026-08-02T10:00' parses as local
  • Non-ISO strings such as '02/08/2026' are browser-dependent — never rely on them
  • === never works on two dates; compare getTime()
  • Adding 86,400,000 milliseconds is 24 hours, which is not always one calendar day
  • Setters mutate the date in place — copy with new Date(original) first
  • Store ISO or epoch; store plain 'YYYY-MM-DD' when only the calendar date matters
Notes
  • When you test date code, test the awkward inputs deliberately: the last day of a month, 29 February in a leap year, and the boundary between two years. Those three cases catch the large majority of date arithmetic mistakes.
Ask AI