Date and Time
Working with dates is unavoidable in real applications — event schedulers, countdowns, activity logs, and timestamps all depend on it. JavaScript's built-in Date object handles this, though it has some quirks worth understanding. Once you know its patterns, you can confidently build anything from a simple clock to a booking system.
Creating Dates
The Date constructor gives you several ways to create a date object.
// Current date and time
const now = new Date();
console.log(now); // e.g. 2026-03-02T14:30:00.000Z
// From a date string. A date-only string is parsed as UTC midnight, but toDateString()
// reports LOCAL time — so west of Greenwich this prints the previous day.
const release = new Date("2024-01-15");
console.log(release.toDateString()); // Mon Jan 15 2024 in UTC+; Sun Jan 14 2024 in the Americas
// From individual components: year, month (0-indexed!), day, hour, minute, second
const meeting = new Date(2026, 2, 15, 9, 30, 0); // March 15, 2026 at 09:30
console.log(meeting.toLocaleString()); // 3/15/2026, 9:30:00 AM
// From a Unix timestamp (milliseconds since Jan 1, 1970)
const epoch = new Date(0);
console.log(epoch.toUTCString()); // Thu, 01 Jan 1970 00:00:00 GMT
// Get the current timestamp as a number
console.log(Date.now()); // e.g. 1740924600000
The most common gotcha: months are zero-indexed. January is 0, December is 11. This trips up even experienced developers.
Reading Date Components
Once you have a Date object, you can extract its individual parts using getter methods.
const event = new Date(2026, 5, 20, 14, 45, 30); // June 20, 2026 at 14:45:30
console.log(event.getFullYear()); // 2026
console.log(event.getMonth()); // 5 (June — zero-indexed)
console.log(event.getDate()); // 20 (day of the month)
console.log(event.getDay()); // 6 (Saturday — 0 is Sunday)
console.log(event.getHours()); // 14
console.log(event.getMinutes()); // 45
console.log(event.getSeconds()); // 30
console.log(event.getTime()); // milliseconds since epoch
// Useful: build a human-readable label
const days = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
const months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
const label = `${days[event.getDay()]}, ${months[event.getMonth()]} ${event.getDate()}, ${event.getFullYear()}`;
console.log(label); // Saturday, Jun 20, 2026
Recall
Without scrolling up: the label above does months[event.getMonth()] with a plain lookup array — no - 1 anywhere. Why does that line up correctly, and what earlier idea makes it work?
Formatting Dates
JavaScript provides built-in formatting through toLocaleDateString, toLocaleTimeString, and the powerful Intl.DateTimeFormat API.
const date = new Date(2026, 2, 2, 9, 5, 0); // March 2, 2026 at 09:05
// Simple locale-aware formatting
console.log(date.toLocaleDateString("en-US")); // 3/2/2026
console.log(date.toLocaleDateString("en-GB")); // 02/03/2026
console.log(date.toLocaleTimeString("en-US")); // 9:05:00 AM
// Full control with Intl.DateTimeFormat
const formatter = new Intl.DateTimeFormat("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});
console.log(formatter.format(date)); // Monday, March 2, 2026
// Compact time display
const timeFormatter = new Intl.DateTimeFormat("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: true,
});
console.log(timeFormatter.format(date)); // 09:05 AM
// ISO string — useful for APIs and storage.
// The Date above was built from LOCAL time, and toISOString() prints UTC, so the
// time shown here shifts with your timezone: 09:05Z in UTC, 08:05Z in UTC+1, and so on.
console.log(date.toISOString()); // e.g. 2026-03-02T08:05:00.000Z in UTC+1
Intl.DateTimeFormat is the modern, locale-aware approach. Prefer it over manual string building.
Comparing and Calculating Dates
Because Date objects expose their underlying timestamp via .getTime(), you can compare and calculate differences using arithmetic.
const start = new Date(2026, 0, 1); // Jan 1, 2026
const end = new Date(2026, 11, 31); // Dec 31, 2026
// Direct comparison using timestamps
console.log(start < end); // true
console.log(start.getTime() === end.getTime()); // false
// Calculate the difference in days
const MS_PER_DAY = 1000 * 60 * 60 * 24;
const diffMs = end.getTime() - start.getTime();
const diffDays = Math.round(diffMs / MS_PER_DAY);
console.log(`Days between: ${diffDays}`); // Days between: 364
// Days until a future date
function daysUntil(targetDate) {
const today = new Date();
today.setHours(0, 0, 0, 0); // normalize to midnight
const diff = targetDate.getTime() - today.getTime();
return Math.ceil(diff / MS_PER_DAY);
}
const nextYear = new Date(2027, 0, 1);
console.log(`Days until 2027: ${daysUntil(nextYear)}`);
Normalizing time to midnight with setHours(0, 0, 0, 0) is important when you only care about calendar dates, not specific times.
Debug
This should log 'Days remaining in December: 30'. Instead the assertion fails — the day count comes out wrong, and nothing warns you why. Predict what daysBetween actually returns, then fix it so the program exits cleanly.
const assert = require('assert');
const MS_PER_DAY = 1000 * 60 * 60 * 24;
// Count whole days between two dates given as strings.
function daysBetween(startISO, endISO) {
const start = new Date(startISO);
const end = new Date(endISO);
return Math.round((end.getTime() - start.getTime()) / MS_PER_DAY);
}
// Intent: days from Dec 1 to Dec 31, 2026 — should be 30.
const days = daysBetween('2026-12-01', '31-12-2026');
assert.strictEqual(days, 30, 'expected 30 days, got ' + days);
console.log('Days remaining in December:', days);Expected output: Days remaining in December: 30
Modifying Dates
Every getter has a matching setter, letting you adjust parts of a date in place.
const deadline = new Date(2026, 2, 2); // March 2, 2026
// Add 30 days
deadline.setDate(deadline.getDate() + 30);
console.log(deadline.toDateString()); // Wed Apr 01 2026
// Add 3 months
const nextQuarter = new Date(2026, 2, 2);
nextQuarter.setMonth(nextQuarter.getMonth() + 3);
console.log(nextQuarter.toDateString()); // Tue Jun 02 2026 (day 2 exists in June, so nothing shifts)
// Build an "expires at" timestamp 1 hour from now
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + 1);
console.log(`Session expires: ${expiresAt.toLocaleTimeString()}`);
JavaScript automatically handles overflow — adding 31 days to January 31st correctly rolls over to March 3rd, or March 2nd in a leap year. The leap year lands you earlier, not later: those 31 days have to cross February first, and a 29-day February absorbs one more of them than a 28-day one, leaving one fewer day to spill into March.
Predict
Trace this by hand before running. It starts from January 2000 and adds 13 months with setUTCMonth. What do the two logs print? (UTC methods are used so the answer is the same in every timezone.)
const anniversary = new Date(Date.UTC(2000, 0, 1)); // month 0 = January
anniversary.setUTCMonth(anniversary.getUTCMonth() + 13);
console.log(anniversary.getUTCFullYear());
console.log(anniversary.getUTCMonth());Try It Yourself
This is a build task: a small program that reports its own pass/fail. Three date utilities are stubbed out with only their signatures — you write the logic. Run it as-is and it fails at the first check; implement each function until it prints All checks passed.
Nearly everything you need is above: whole-millisecond arithmetic for the duration (formatDuration), getTime() differences over MS_PER_DAY for the day count (daysBetween, the same timestamp arithmetic from Comparing and Calculating Dates), and a setUTCDate roll with toISOString for the date shift (addDays — Modifying Dates showed the local setDate and the Predict block used setUTCMonth; the UTC date setter works the same way, kept in UTC so no timezone can move the answer). The spec for each is in the prompt; the checks below the divider are fixed.
Build
Finish the build. Three functions are stubbed with only their signatures; the checks below them fail until each returns the right value. Spec: formatDuration(ms) renders a positive whole-millisecond duration as 'Hh MMm' — hours unpadded, minutes ALWAYS two digits with a leading zero, leftover seconds dropped, so 7500000 becomes '2h 05m'. daysBetween(isoA, isoB) returns the whole number of days from isoA to isoB (both YYYY-MM-DD) using getTime on each. addDays(iso, n) returns the YYYY-MM-DD that is n days after iso, using UTC methods so the result never shifts by timezone. Checks run top to bottom, so the first failure is TODO 1 — implement it first, then work down.
const assert = require('assert');
const MS_PER_DAY = 1000 * 60 * 60 * 24;
// TODO 1: formatDuration(ms) -> "2h 05m" (minutes padded to two digits)
function formatDuration(ms) {
// your code here
}
// TODO 2: daysBetween(isoA, isoB) -> whole days from isoA to isoB
function daysBetween(isoA, isoB) {
// your code here
}
// TODO 3: addDays(iso, n) -> the YYYY-MM-DD n days after iso, in UTC
function addDays(iso, n) {
// your code here
}
// --- Build checks: these must all pass. Do not edit below this line. ---
assert.strictEqual(
formatDuration(7500000),
"2h 05m",
"formatDuration(7500000) should render 2 hours 5 minutes as '2h 05m' with a padded minute",
);
assert.strictEqual(
daysBetween("2026-01-01", "2026-01-15"),
14,
"daysBetween('2026-01-01', '2026-01-15') should be 14 whole days",
);
assert.strictEqual(
addDays("2026-01-30", 5),
"2026-02-04",
"addDays('2026-01-30', 5) should roll across the month boundary to '2026-02-04'",
);
console.log("All checks passed.");
console.log("Duration:", formatDuration(7500000));
console.log("Days between:", daysBetween("2026-01-01", "2026-01-15"));
console.log("Add days:", addDays("2026-01-30", 5));Expected output: All checks passed.
Duration: 2h 05m
Days between: 14
Add days: 2026-02-04
Once it passes, try two variations and predict each before running:
- An exact hour. Call
formatDuration(3600000)(exactly one hour, zero minutes). Predict the string before running. It returns"1h 00m", not"1h 0m"— thepadStart(2, "0")fills the empty minute slot, which is the whole reason the format pins two digits. - Reversed dates. Call
daysBetween("2026-01-15", "2026-01-01")with the arguments swapped. Predict the number before running. You get-14:getTime()subtraction is directional, so an end earlier than the start yields a negative day count — the sign tells you the order, and it is identical in every timezone because the timestamps are absolute.
Key Takeaways
new Date()returns the current date and time;Date.now()returns the raw millisecond timestamp- Months are zero-indexed — January is
0, December is11 - Use
getTime()to convert a date to a number for arithmetic and comparisons - Use
Intl.DateTimeFormatfor locale-aware, user-friendly date formatting - Normalize dates to midnight with
setHours(0, 0, 0, 0)when comparing calendar days toISOString()produces a consistent, timezone-neutral string suitable for APIs and storage- JavaScript handles date overflow automatically — adding too many days or months rolls into the next period correctly
Pro Tip: For production applications that handle timezones, recurring events, or complex date math, reach for a library like
date-fns, or useTemporal— a modern built-in date/time API (TC39 Stage 4) that is landing in JavaScript engines now. Check support before relying on it. The built-inDateobject uses local timezone by default and can behave unexpectedly across regions — always usetoISOString()or UTC methods when storing or transmitting dates between systems.
Next Steps
Now that you can work with dates and times, the next lesson explores generators and iterators — a powerful protocol for producing sequences of values on demand, enabling lazy evaluation and infinite sequences.
Next lesson
Generators and Iterators
Learn JavaScript generators and iterators to produce values on demand. Unlock lazy evaluation, infinite sequences, and async patterns.
25 min