Skip to main content

How to Work with Dates and Time in JavaScript

Working with dates and time is one of the most notoriously tricky areas in programming, and JavaScript's Date object has earned a special reputation for its quirks. Months start from zero, time zones create subtle bugs, parsing rules vary across browsers, and formatting options are limited without external help.

Despite its imperfections, the Date object is deeply embedded in JavaScript and understanding it thoroughly is essential. You need dates for timestamps, scheduling, calculating durations, displaying formatted dates to users, comparing events, building calendars, and countless other tasks. The key to working with dates effectively is knowing the pitfalls in advance so you can avoid them.

This guide covers every aspect of JavaScript's Date object: creating dates in multiple ways, reading and modifying components, date arithmetic, formatting for display, parsing strings, performance benchmarking, and the common mistakes that trip up developers at every level. It also looks ahead to the Temporal API and the library ecosystem for cases when native Date is not enough.

Creating Dates: new Date(), Timestamps, Date Strings

JavaScript provides several ways to create Date objects, each suited to different situations.

new Date(): Current Date and Time

Called without arguments, new Date() creates a Date object representing the current date and time:

let now = new Date();
console.log(now);
// Something like: 2026-03-03T21:32:00.109Z (depends on when you run it)

new Date(milliseconds) From Timestamp

A timestamp is the number of milliseconds since January 1, 1970, 00:00:00 UTC (the Unix epoch). You can create a Date from a timestamp:

// January 1, 1970 00:00:00 UTC
let epoch = new Date(0);
console.log(epoch); // 1970-01-01T00:00:00.000Z

// 24 hours after the epoch
let dayLater = new Date(24 * 60 * 60 * 1000);
console.log(dayLater); // 1970-01-02T00:00:00.000Z

// Negative timestamps go before 1970
let beforeEpoch = new Date(-24 * 60 * 60 * 1000);
console.log(beforeEpoch); // 1969-12-31T00:00:00.000Z

// A specific known timestamp
let specific = new Date(1705312800000);
console.log(specific); // 2024-01-15T10:00:00.000Z

new Date(year, month, day, hours, minutes, seconds, ms)

Create a Date from individual components. All components use local time, not UTC:

// January 15, 2024 (month is 0-indexed!)
let date = new Date(2024, 0, 15);
console.log(date); // Mon Jan 15 2024 00:00:00 (local time)

// January 15, 2024, 14:30:45.500
let precise = new Date(2024, 0, 15, 14, 30, 45, 500);
console.log(precise); // Mon Jan 15 2024 14:30:45 (local time)

Required arguments: Only year and month are required. Missing components default to:

  • day: 1
  • hours, minutes, seconds, ms: 0
// Only year and month: defaults day to 1, time to 00:00:00
let feb2024 = new Date(2024, 1); // February 1, 2024
console.log(feb2024); // Thu Feb 01 2024 00:00:00

// Two-digit years are treated as 1900+
let old = new Date(99, 0, 1);
console.log(old.getFullYear()); // 1999 (not 99 or 2099!)
Month Is 0-Indexed

January is 0, February is 1, ..., December is 11. This is the single most common source of date bugs in JavaScript. Every developer gets bitten by this at least once:

// ❌ Common mistake: this creates FEBRUARY 15, not January 15!
let wrong = new Date(2024, 1, 15);
console.log(wrong); // Thu Feb 15 2024

// ✅ Correct: January is month 0
let correct = new Date(2024, 0, 15);
console.log(correct); // Mon Jan 15 2024

new Date(dateString) From String

You can pass a date string to the constructor. The string is parsed using Date.parse() internally:

// ISO 8601 format (most reliable across all environments)
let iso = new Date("2024-01-15");
console.log(iso); // Mon Jan 15 2024 (time zone depends on implementation!)

let isoFull = new Date("2024-01-15T14:30:00Z");
console.log(isoFull); // Mon Jan 15 2024 14:30:00 UTC

let isoOffset = new Date("2024-01-15T14:30:00+01:00");
console.log(isoOffset); // Mon Jan 15 2024 13:30:00 UTC
Date String Parsing Is Inconsistent

Date-only strings like "2024-01-15" are treated as UTC by the specification, while date-time strings without a timezone are treated as local time. This inconsistency causes bugs:

// Date-only: treated as UTC midnight
let dateOnly = new Date("2024-01-15");
// In UTC-5 timezone: Sun Jan 14 2024 19:00:00 (the day before!)

// Date-time without timezone: treated as local time
let dateTime = new Date("2024-01-15T00:00:00");
// In any timezone: Mon Jan 15 2024 00:00:00 (local midnight)

For predictable behavior, always include the time and timezone, or use the component constructor.

Date.now() for Current Timestamp

Date.now() returns the current timestamp in milliseconds without creating a Date object. It is more efficient than new Date().getTime():

let timestamp = Date.now();
console.log(timestamp); // for example 1772573740571

// Equivalent but less efficient:
let timestamp2 = new Date().getTime();
console.log(timestamp2); // Same value as before (1772573740571)

// Useful for timing
let start = Date.now();
// ... do some work ...
let end = Date.now();
console.log(`Operation took ${end - start}ms`);

Getters: Reading Date Components

Date objects have getter methods for every component. Each comes in two variants: local time and UTC.

Local Time Getters

let date = new Date(2024, 0, 15, 14, 30, 45, 500);
// Mon Jan 15 2024 14:30:45.500 (local time)

console.log(date.getFullYear()); // 2024
console.log(date.getMonth()); // 0 (January - 0-indexed!)
console.log(date.getDate()); // 15 (day of month, 1-31)
console.log(date.getDay()); // 1 (day of week: 0=Sunday, 1=Monday, ..., 6=Saturday)
console.log(date.getHours()); // 14
console.log(date.getMinutes()); // 30
console.log(date.getSeconds()); // 45
console.log(date.getMilliseconds()); // 500
console.log(date.getTime()); // Timestamp in milliseconds since epoch
console.log(date.getTimezoneOffset()); // Minutes offset from UTC (e.g., -60 for UTC+1)

UTC Getters

let date = new Date("2024-01-15T14:30:45.500Z"); // Explicitly UTC

console.log(date.getUTCFullYear()); // 2024
console.log(date.getUTCMonth()); // 0
console.log(date.getUTCDate()); // 15
console.log(date.getUTCDay()); // 1
console.log(date.getUTCHours()); // 14
console.log(date.getUTCMinutes()); // 30
console.log(date.getUTCSeconds()); // 45
console.log(date.getUTCMilliseconds()); // 500

Day of Week Reference

getDay() returns 0 for Sunday through 6 for Saturday:

let dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

let date = new Date(2024, 0, 15); // Monday
console.log(dayNames[date.getDay()]); // "Monday"
getYear() Is Deprecated

Never use getYear(). It returns the year minus 1900 (e.g., 124 for 2024). Always use getFullYear():

let date = new Date(2024, 0, 15);

console.log(date.getYear()); // 124 (deprecated, wrong!)
console.log(date.getFullYear()); // 2024 (correct)

Setters: Modifying Date Components

Every getter has a corresponding setter that modifies the date in place. Setters also come in local and UTC variants.

let date = new Date(2024, 0, 15, 14, 30, 0);
console.log(date); // Mon Jan 15 2024 14:30:00

date.setFullYear(2025);
console.log(date); // Wed Jan 15 2025 14:30:00

date.setMonth(5); // June (0-indexed!)
console.log(date); // Sun Jun 15 2025 14:30:00

date.setDate(20);
console.log(date); // Fri Jun 20 2025 14:30:00

date.setHours(9);
console.log(date); // Fri Jun 20 2025 09:30:00

date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
console.log(date); // Fri Jun 20 2025 09:00:00

Setting Multiple Components

Some setters accept multiple arguments for setting several related components at once:

let date = new Date();

// setHours(hours, minutes, seconds, ms)
date.setHours(14, 30, 45, 0);

// setFullYear(year, month, day)
date.setFullYear(2025, 5, 15); // June 15, 2025

// setMonth(month, day)
date.setMonth(11, 25); // December 25

UTC Setters

let date = new Date("2024-01-15T14:30:00Z");

date.setUTCHours(10);
date.setUTCDate(20);
console.log(date.toISOString()); // "2024-01-20T10:30:00.000Z"

Autocorrection of Out-of-Range Values

One of the most useful features of JavaScript's Date is autocorrection. When you set a component to an out-of-range value, the Date automatically adjusts the other components accordingly:

// February 30 doesn't exist, autocorrects to March 1 (or 2 in non-leap year)
let date = new Date(2024, 1, 30); // Feb 30, 2024
console.log(date); // Fri Mar 01 2024 (2024 is a leap year, so Feb has 29 days)

let date2 = new Date(2023, 1, 30); // Feb 30, 2023
console.log(date2); // Thu Mar 02 2023 (2023 is not a leap year, Feb has 28 days)

// Day 0 means "last day of previous month"
let lastDayOfJan = new Date(2024, 1, 0); // Feb 0 = Jan 31
console.log(lastDayOfJan); // Wed Jan 31 2024

// Negative days go further back
let twoDaysBefore = new Date(2024, 1, -1); // Feb -1 = Jan 30
console.log(twoDaysBefore); // Tue Jan 30 2024

Finding the Last Day of Any Month

function getLastDayOfMonth(year, month) {
// month + 1 = next month, day 0 = last day of previous month
return new Date(year, month + 1, 0).getDate();
}

console.log(getLastDayOfMonth(2024, 0)); // 31 (January)
console.log(getLastDayOfMonth(2024, 1)); // 29 (February 2024, a leap year)
console.log(getLastDayOfMonth(2023, 1)); // 28 (February 2023)
console.log(getLastDayOfMonth(2024, 3)); // 30 (April)

Autocorrection with Setters

Autocorrection also works when using setter methods:

let date = new Date(2024, 0, 31); // Jan 31
console.log(date); // Wed Jan 31 2024

date.setDate(date.getDate() + 1); // Add 1 day
console.log(date); // Thu Feb 01 2024 (automatically rolls to February)

date.setDate(date.getDate() + 30); // Add 30 days
console.log(date); // Sat Mar 02 2024 (rolls to March)

// Hours can overflow to the next day
date.setHours(date.getHours() + 48); // Add 48 hours
console.log(date); // Mon Mar 04 2024

Adding Months Safely

function addMonths(date, months) {
let result = new Date(date);
let targetMonth = result.getMonth() + months;
result.setMonth(targetMonth);

// Handle month overflow (e.g., Jan 31 + 1 month = Mar 3, not Feb 31)
// If the day changed, it means overflow happened
if (result.getDate() !== date.getDate()) {
result.setDate(0); // Go to last day of previous month
}

return result;
}

let jan31 = new Date(2024, 0, 31);
console.log(addMonths(jan31, 1)); // Feb 29, 2024 (not Mar 2)
console.log(addMonths(jan31, 2)); // Mar 31, 2024

Date-to-Number Conversion (Timestamps)

When a Date object is converted to a number, it becomes its timestamp (milliseconds since epoch):

let date = new Date(2024, 0, 15, 12, 0, 0);

// Explicit conversion
console.log(date.getTime()); // 1705312800000
console.log(Number(date)); // 1705312800000
console.log(+date); // 1705312800000

// Implicit conversion in arithmetic
let date1 = new Date(2024, 0, 15);
let date2 = new Date(2024, 0, 20);

console.log(date2 - date1); // 432000000 (5 days in milliseconds)
// 5 * 24 * 60 * 60 * 1000 = 432,000,000

This conversion is the foundation of date arithmetic and comparison.

Date Arithmetic (Calculating Differences)

Difference Between Two Dates

let start = new Date(2024, 0, 1);               // Jan 1, 2024
let end = new Date(2024, 11, 31); // Dec 31, 2024

let diffMs = end - start; // Milliseconds
let diffSeconds = diffMs / 1000; // Seconds
let diffMinutes = diffMs / (1000 * 60); // Minutes
let diffHours = diffMs / (1000 * 60 * 60); // Hours
let diffDays = diffMs / (1000 * 60 * 60 * 24); // Days

console.log(`${diffDays} days`); // 365 days
console.log(`${diffHours} hours`); // 8760 hours

Utility Function: Human-Readable Difference

function timeSince(date) {
let seconds = Math.floor((Date.now() - date) / 1000);

let intervals = [
{ label: "year", seconds: 31536000 },
{ label: "month", seconds: 2592000 },
{ label: "week", seconds: 604800 },
{ label: "day", seconds: 86400 },
{ label: "hour", seconds: 3600 },
{ label: "minute", seconds: 60 },
{ label: "second", seconds: 1 }
];

for (let { label, seconds: intervalSec } of intervals) {
let count = Math.floor(seconds / intervalSec);
if (count >= 1) {
return `${count} ${label}${count !== 1 ? "s" : ""} ago`;
}
}

return "just now";
}

console.log(timeSince(new Date(Date.now() - 3600000))); // "1 hour ago"
console.log(timeSince(new Date(Date.now() - 86400000 * 3))); // "3 days ago"

Adding Time to a Date

function addDays(date, days) {
let result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}

function addHours(date, hours) {
return new Date(date.getTime() + hours * 3600000);
}

function addMinutes(date, minutes) {
return new Date(date.getTime() + minutes * 60000);
}

let now = new Date();
console.log(addDays(now, 7)); // 1 week from now
console.log(addDays(now, -30)); // 30 days ago
console.log(addHours(now, 2.5)); // 2.5 hours from now

Comparing Dates

let date1 = new Date(2024, 0, 15);
let date2 = new Date(2024, 5, 20);

// Comparison operators work (dates are converted to timestamps)
console.log(date1 < date2); // true
console.log(date1 > date2); // false

// Equality requires getTime(), objects are compared by reference
console.log(date1 === date2); // false (different objects)
console.log(date1.getTime() === date2.getTime()); // false (different timestamps)

let date3 = new Date(2024, 0, 15);
console.log(date1.getTime() === date3.getTime()); // true (same timestamp)

Formatting Dates

Built-In toString Methods

let date = new Date(2024, 0, 15, 14, 30, 45);

console.log(date.toString());
// "Mon Jan 15 2024 14:30:45 GMT+0100 (Central European Standard Time)"

console.log(date.toISOString());
// "2024-01-15T13:30:45.000Z" (always UTC, ISO 8601)

console.log(date.toUTCString());
// "Mon, 15 Jan 2024 13:30:45 GMT"

console.log(date.toDateString());
// "Mon Jan 15 2024"

console.log(date.toTimeString());
// "14:30:45 GMT+0100 (Central European Standard Time)"

console.log(date.toJSON());
// "2024-01-15T13:30:45.000Z" (same as toISOString)

toLocaleDateString and toLocaleTimeString

These methods format dates according to locale conventions:

let date = new Date(2024, 0, 15, 14, 30, 45);

// Different locales
console.log(date.toLocaleDateString("en-US")); // "1/15/2024"
console.log(date.toLocaleDateString("en-GB")); // "15/01/2024"
console.log(date.toLocaleDateString("de-DE")); // "15.1.2024"
console.log(date.toLocaleDateString("ja-JP")); // "2024/1/15"
console.log(date.toLocaleDateString("it-IT")); // "15/1/2024"

// With options
console.log(date.toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric"
}));
// "Monday, January 15, 2024"

console.log(date.toLocaleTimeString("en-US"));
// "2:30:45 PM"

console.log(date.toLocaleTimeString("de-DE"));
// "14:30:45"

// Combined date and time
console.log(date.toLocaleString("en-US", {
dateStyle: "medium",
timeStyle: "short"
}));
// "Jan 15, 2024, 2:30 PM"

Intl.DateTimeFormat: Reusable Formatter

For repeated formatting (like in a loop), creating an Intl.DateTimeFormat is more efficient:

let formatter = new Intl.DateTimeFormat("en-US", {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
timeZone: "America/New_York"
});

let dates = [
new Date("2024-01-15T14:30:00Z"),
new Date("2024-06-20T08:00:00Z"),
new Date("2024-12-25T00:00:00Z")
];

dates.forEach(d => console.log(formatter.format(d)));
// "Mon, Jan 15, 2024, 09:30 AM"
// "Thu, Jun 20, 2024, 04:00 AM"
// "Tue, Dec 24, 2024, 07:00 PM"

// Format to parts (useful for custom layouts)
let parts = formatter.formatToParts(dates[0]);
console.log(parts);
// [
// { type: "weekday", value: "Mon" },
// { type: "literal", value: ", " },
// { type: "month", value: "Jan" },
// ...
// ]

Common Formatting Options

let date = new Date(2024, 0, 15, 14, 30, 0);

// Short date
console.log(date.toLocaleDateString("en-US", { dateStyle: "short" }));
// "1/15/24"

// Medium date
console.log(date.toLocaleDateString("en-US", { dateStyle: "medium" }));
// "Jan 15, 2024"

// Long date
console.log(date.toLocaleDateString("en-US", { dateStyle: "long" }));
// "January 15, 2024"

// Full date
console.log(date.toLocaleDateString("en-US", { dateStyle: "full" }));
// "Monday, January 15, 2024"

// Relative time (using Intl.RelativeTimeFormat)
let rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
console.log(rtf.format(-1, "day")); // "yesterday"
console.log(rtf.format(2, "day")); // "in 2 days"
console.log(rtf.format(-3, "hour")); // "3 hours ago"
console.log(rtf.format(0, "day")); // "today"

Manual Formatting (When You Need Exact Control)

function formatDate(date, format) {
let pad = (n) => String(n).padStart(2, "0");

let replacements = {
"YYYY": date.getFullYear(),
"MM": pad(date.getMonth() + 1),
"DD": pad(date.getDate()),
"HH": pad(date.getHours()),
"mm": pad(date.getMinutes()),
"ss": pad(date.getSeconds())
};

let result = format;
for (let [token, value] of Object.entries(replacements)) {
result = result.replace(token, value);
}
return result;
}

let date = new Date(2024, 0, 5, 9, 3, 7);

console.log(formatDate(date, "YYYY-MM-DD")); // "2024-01-05"
console.log(formatDate(date, "DD/MM/YYYY")); // "05/01/2024"
console.log(formatDate(date, "YYYY-MM-DD HH:mm:ss")); // "2024-01-05 09:03:07"

Parsing Date Strings: Date.parse()

Date.parse() takes a date string and returns its timestamp, or NaN if the string cannot be parsed:

// ISO 8601 (most reliable)
console.log(Date.parse("2024-01-15T14:30:00Z")); // 1705325400000
console.log(Date.parse("2024-01-15T14:30:00+01:00")); // 1705321800000
console.log(Date.parse("2024-01-15")); // 1705276800000

// Other formats (behavior varies by browser!)
console.log(Date.parse("Jan 15, 2024")); // Works in most browsers
console.log(Date.parse("15 Jan 2024")); // Works in most browsers
console.log(Date.parse("January 15, 2024 14:30:00")); // Works in most browsers
console.log(Date.parse("2024/01/15")); // May work, not standardized

// Invalid strings
console.log(Date.parse("not a date")); // NaN
console.log(Date.parse("")); // NaN
Only Trust ISO 8601 Format

The only date string format guaranteed to work consistently across all JavaScript environments is ISO 8601 (YYYY-MM-DDTHH:mm:ss.sssZ). All other formats depend on browser implementation and may produce different results or NaN in different environments. For anything other than ISO 8601, parse manually or use a library.

Benchmarking with Dates

Date.now() provides a simple way to measure code performance:

function benchmark(fn, iterations = 1000000) {
let start = Date.now();

for (let i = 0; i < iterations; i++) {
fn();
}

let end = Date.now();
return end - start;
}

// Compare two approaches
let time1 = benchmark(() => {
let sum = 0;
for (let i = 0; i < 100; i++) sum += i;
});

let time2 = benchmark(() => {
let sum = Array.from({ length: 100 }, (_, i) => i).reduce((a, b) => a + b, 0);
});

console.log(`Loop: ${time1}ms`);
console.log(`Array: ${time2}ms`);

performance.now() for Better Precision

For more precise measurements, use performance.now(), which provides sub-millisecond resolution:

let start = performance.now();

// ... code to benchmark ...

let end = performance.now();
console.log(`Took ${(end - start).toFixed(3)}ms`);

// performance.now() returns microsecond precision
// Date.now() only has millisecond precision
Use performance.now() for Benchmarking

performance.now() is more accurate than Date.now() for benchmarking because it provides sub-millisecond precision and is not affected by system clock adjustments. Date.now() is sufficient for timestamps and general timing.

The Temporal API (TC39 Proposal)

The Temporal API is a proposed replacement for the Date object that addresses all of its major shortcomings. While still at Stage 3 in the TC39 process (as of 2024), it is expected to become part of JavaScript in the near future.

Why Temporal?

The Date object has fundamental design flaws:

  • Months are 0-indexed
  • No built-in support for time zones beyond the local zone and UTC
  • Mutable (setters change the object in place)
  • Ambiguous parsing
  • No support for calendar systems
  • No duration or interval type

What Temporal Provides (Preview)

// Note: Temporal is not yet available in all environments
// This shows the expected API

// Temporal.Now: current date/time
// Temporal.Now.instant() → current instant in time
// Temporal.Now.zonedDateTimeISO() → current date/time in local zone
// Temporal.Now.plainDateISO() → current date (no time, no zone)

// Temporal.PlainDate: date without time or zone
// let date = Temporal.PlainDate.from("2024-01-15");
// date.month → 1 (1-indexed! Finally!)
// date.add({ days: 30 }) → new PlainDate (immutable)

// Temporal.PlainTime: time without date or zone
// let time = Temporal.PlainTime.from("14:30:00");

// Temporal.ZonedDateTime: full date/time with zone
// let zdt = Temporal.ZonedDateTime.from("2024-01-15T14:30[America/New_York]");

// Temporal.Duration: represents a length of time
// let duration = Temporal.Duration.from({ hours: 2, minutes: 30 });

// Temporal.Instant: exact moment in time (like a timestamp)
// let instant = Temporal.Instant.from("2024-01-15T14:30:00Z");

Key improvements:

  • 1-indexed months (January = 1)
  • Immutable objects (all operations return new instances)
  • Explicit time zones (no more ambiguity)
  • Separate types for different concepts (date, time, datetime, instant, duration)
  • Consistent parsing rules

Libraries: date-fns, Day.js, Luxon

Until Temporal is widely available, third-party libraries fill the gaps.

Day.js (Lightweight, Moment-Compatible)

Tiny (2KB), immutable, with a Moment.js-compatible API:

import dayjs from "dayjs";

let now = dayjs();
console.log(now.format("YYYY-MM-DD HH:mm:ss")); // "2024-01-15 14:30:45"

let nextWeek = now.add(7, "day");
let diff = nextWeek.diff(now, "day");

console.log(now.isBefore(nextWeek)); // true
console.log(now.format("dddd, MMMM D, YYYY")); // "Monday, January 15, 2024"

date-fns (Functional, Tree-Shakeable)

Individual functions that work with native Date objects:

import { format, addDays, differenceInDays, isAfter } from "date-fns";

let now = new Date();
console.log(format(now, "yyyy-MM-dd HH:mm:ss"));

let nextWeek = addDays(now, 7);
console.log(differenceInDays(nextWeek, now)); // 7
console.log(isAfter(nextWeek, now)); // true

Built by a Moment.js maintainer, with first-class time zone support:

import { DateTime } from "luxon";

let now = DateTime.now();
console.log(now.toFormat("yyyy-MM-dd HH:mm:ss"));

let tokyo = now.setZone("Asia/Tokyo");
console.log(tokyo.toFormat("HH:mm ZZZZ")); // "23:30 Japan Standard Time"

let duration = now.plus({ weeks: 2, days: 3 });

When to Use a Library

ScenarioRecommended Approach
Simple timestamp or date displayNative Date + Intl.DateTimeFormat
Complex formatting with patternsDay.js or date-fns
Time zone conversionsLuxon
Date arithmetic with edge casesdate-fns or Luxon
Bundle size is criticalDay.js (2KB) or date-fns (tree-shakeable)
Full Moment.js replacementDay.js (similar API)

Common Mistakes: Month Is 0-Indexed, Time Zones

Mistake 1: Wrong Month

The most common date bug in JavaScript:

// ❌ WRONG: Trying to create March 1, 2024
let wrong = new Date(2024, 3, 1); // This is APRIL 1!
console.log(wrong.toLocaleDateString("en-US")); // "4/1/2024"

// ✅ CORRECT: March is month 2
let correct = new Date(2024, 2, 1);
console.log(correct.toLocaleDateString("en-US")); // "3/1/2024"

Memory aid: Create a constant or helper to make months readable:

const MONTHS = {
JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5,
JUL: 6, AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11
};

let christmas = new Date(2024, MONTHS.DEC, 25);
console.log(christmas.toLocaleDateString("en-US")); // "12/25/2024"

Mistake 2: Date-Only Strings Treated as UTC

// ❌ Surprising behavior: date-only string is parsed as UTC midnight
let date = new Date("2024-01-15");
console.log(date.getDate()); // 14 or 15 depending on your timezone!

// In UTC-5: the UTC midnight of Jan 15 is Jan 14 at 7pm local time
// getDate() returns the LOCAL day, which could be 14!

// ✅ Use the component constructor for local dates
let localDate = new Date(2024, 0, 15); // Always Jan 15 in local time
console.log(localDate.getDate()); // Always 15

Mistake 3: Comparing Dates with ===

let a = new Date(2024, 0, 15);
let b = new Date(2024, 0, 15);

// ❌ Reference comparison: always false for different objects
console.log(a === b); // false

// ✅ Compare timestamps
console.log(a.getTime() === b.getTime()); // true
console.log(+a === +b); // true (unary + converts to timestamp)

Mistake 4: Mutating Dates Unintentionally

function addDays(date, days) {
// ❌ WRONG: Mutates the original date!
date.setDate(date.getDate() + days);
return date;
}

let birthday = new Date(2024, 0, 15);
let partyDate = addDays(birthday, 3);

console.log(birthday.getDate()); // 18 (birthday was changed!)
console.log(partyDate.getDate()); // 18 (same object!)

// ✅ CORRECT: Create a new Date
function addDaysSafe(date, days) {
let result = new Date(date); // Copy first
result.setDate(result.getDate() + days);
return result;
}

let birthday2 = new Date(2024, 0, 15);
let partyDate2 = addDaysSafe(birthday2, 3);

console.log(birthday2.getDate()); // 15 (unchanged)
console.log(partyDate2.getDate()); // 18 (separate object)

Mistake 5: Assuming Local Time in ISO Strings

// This is UTC 14:00, not local 14:00
let utcDate = new Date("2024-01-15T14:00:00Z");
console.log(utcDate.getHours()); // Varies by timezone! Not always 14.

// In UTC+1: getHours() returns 15
// In UTC-5: getHours() returns 9
// In UTC+0: getHours() returns 14

// ✅ Use getUTCHours() for UTC times
console.log(utcDate.getUTCHours()); // Always 14

Summary

  • Create dates with new Date() (now), new Date(ms) (timestamp), new Date(year, month, ...) (components), or new Date(string) (parsing).
  • Month is 0-indexed: January is 0, December is 11. This is the most common source of date bugs.
  • Date.now() returns the current timestamp without creating a Date object. More efficient for timing.
  • Getters (getFullYear, getMonth, getDate, getHours, etc.) read components in local time. UTC variants (getUTCFullYear, etc.) read in UTC.
  • Setters (setFullYear, setMonth, etc.) modify dates in place. Always copy the date first if you need to preserve the original.
  • Autocorrection handles out-of-range values gracefully. Day 0 means "last day of previous month." This is useful for finding month lengths and doing date arithmetic.
  • Dates convert to timestamps (numbers) automatically, enabling arithmetic and comparison with <, >, -. Use getTime() for equality checks (not ===).
  • Format dates with toLocaleDateString() and Intl.DateTimeFormat for locale-aware display. Use toISOString() for storage and API communication.
  • Only parse ISO 8601 strings reliably. Other formats behave inconsistently across browsers.
  • Use performance.now() instead of Date.now() for precise benchmarking.
  • The Temporal API will eventually replace Date with immutable objects, 1-indexed months, proper time zone support, and separate types for different concepts.
  • Libraries like Day.js, date-fns, and Luxon fill the gaps until Temporal arrives. Choose based on your needs: Day.js for size, date-fns for tree-shaking, Luxon for time zones.
  • Always copy dates before modifying them with setters. Dates are mutable objects, and passing them around creates shared references.