Skip to content

Why Does JavaScript new Date() Return Unexpected Dates

I wanted to create a simple date in JavaScript. February 2, 2025. How hard could it be?

first-attempt.js
const date = new Date(25, 2, 2)
console.log(date.toDateString())
// Output: "Sun Mar 02 1925"

Wait, what? I got March 2, 1925. I asked for February 2, 2025.

This is the first of many JavaScript Date constructor gotchas that have caused bugs in production codebases for decades. Let me walk through each one.

Gotcha 1: Two-Digit Years Are Offset From 1900

When I passed 25 as the year, JavaScript interpreted it as 1900 + 25 = 1925. This behavior comes from the original Java java.util.Date design (now deprecated in Java, but inherited by JavaScript).

year-gotcha.js
// WRONG: Two-digit years become 1900s
new Date(25, 2, 2) // March 2, 1925
new Date(99, 0, 1) // January 1, 1999
new Date(0, 0, 1) // January 1, 1900
// CORRECT: Always use 4-digit years
new Date(2025, 2, 2) // March 2, 2025
new Date(1999, 0, 1) // January 1, 1999

Why this exists: Legacy design from early Java compatibility. The designers thought two-digit years would be convenient for the 1900s. They were wrong.

How to avoid: Always use 4-digit years in the numeric constructor.

Gotcha 2: Months Are Zero-Indexed (0-11)

Did you notice my original example returned March instead of February? I passed 2 thinking it meant February, but in JavaScript, months are zero-indexed.

month-gotcha.js
// Months are 0-11, not 1-12
new Date(2025, 0, 1) // January 1, 2025 (month 0)
new Date(2025, 1, 1) // February 1, 2025 (month 1)
new Date(2025, 2, 1) // March 1, 2025 (month 2)
new Date(2025, 11, 1) // December 1, 2025 (month 11)
// What I wrote (WRONG):
new Date(2025, 2, 2) // March 2, 2025 - NOT February!
// What I meant (CORRECT):
new Date(2025, 1, 2) // February 2, 2025

Why this exists: Also inherited from Java’s java.util.Date. The designers claimed this allows easier array indexing for month names. In practice, it causes countless off-by-one bugs.

How to avoid: I create a helper function that uses 1-12 for human readability:

safe-date-helper.js
function createDate(year, month, day) {
// month is 1-12 (human readable)
return new Date(year, month - 1, day)
}
createDate(2025, 2, 2) // February 2, 2025 - no surprises

Gotcha 3: Day Overflow and Underflow

I assumed invalid day values would throw an error. Instead, JavaScript silently rolls over to adjacent months.

day-overflow.js
// Day 0 = last day of previous month
new Date(2025, 2, 0) // February 28, 2025 (day 0 of March = last day of Feb)
new Date(2025, 3, 0) // March 31, 2025
new Date(2025, 1, 0) // December 31, 2024 (day 0 of January)
// Day overflow rolls to next month
new Date(2025, 2, 32) // April 1, 2025 (March has 31 days, day 32 = April 1)
new Date(2025, 1, 30) // March 2, 2025 (February 2025 has 28 days)
// Even negative days work
new Date(2025, 2, -1) // February 27, 2025

Why this exists: The spec allows this for “flexibility” in date calculations. It can be useful for finding the last day of a month, but mostly it hides bugs.

How to avoid: Validate day values before creating dates, or use this behavior deliberately:

last-day-of-month.js
// Intentional use: Get last day of any month
function getLastDayOfMonth(year, month) {
// month is 1-12
return new Date(year, month, 0).getDate()
}
getLastDayOfMonth(2025, 2) // 28 (February 2025)
getLastDayOfMonth(2024, 2) // 29 (February 2024 - leap year)
getLastDayOfMonth(2025, 4) // 30 (April)

Gotcha 4: String Parsing Timezone Inconsistency

I tried using string parsing to avoid the month confusion. But strings have their own gotchas.

string-parsing.js
// ISO 8601 format = UTC timezone
const utc = new Date('2025-02-02')
console.log(utc.toString())
// In PST: "Sat Feb 01 2025 16:00:00 GMT-0800" (previous day!)
// Non-ISO format = local timezone
const local = new Date('2025-2-2')
console.log(local.toString())
// In PST: "Sun Feb 02 2025 00:00:00 GMT-0800" (correct day)
// Adding time component changes interpretation
const withTime = new Date('2025-02-02T00:00:00')
console.log(withTime.toString())
// In PST: "Sun Feb 02 2025 00:00:00 GMT-0800" (local time!)

Why this exists: The ECMAScript spec says ISO 8601 date-only strings (YYYY-MM-DD) should be parsed as UTC, while date-time strings should be parsed as local time. Non-ISO strings use implementation-specific rules (usually local time).

How to avoid: Always be explicit about timezone, and prefer ISO 8601 format:

timezone-best-practices.js
// For UTC dates, use ISO format WITHOUT time component
const utcMidnight = new Date('2025-02-02')
// This is Feb 2, 2025 00:00:00 UTC
// For local dates, add time component
const localMidnight = new Date('2025-02-02T00:00:00')
// This is Feb 2, 2025 00:00:00 in YOUR timezone
// Or use numeric constructor (always local time)
const numericLocal = new Date(2025, 1, 2) // Remember: month 1 = February
// This is Feb 2, 2025 00:00:00 in YOUR timezone

Gotcha 5: Different Constructor Signatures Behave Differently

The Date constructor accepts multiple argument patterns, and each behaves differently.

constructor-signatures.js
// No arguments = now
new Date() // Current date and time
// One argument: timestamp (milliseconds since epoch)
new Date(1740000000000) // Specific moment in time
// One argument: string (parsed)
new Date('2025-02-02') // Parsed string
// Two+ arguments: year, month, day, hours, minutes, seconds, ms
new Date(2025, 1, 2) // Feb 2, 2025 00:00:00 local
new Date(2025, 1, 2, 12) // Feb 2, 2025 12:00:00 local
new Date(2025, 1, 2, 12, 30) // Feb 2, 2025 12:30:00 local
// BUT: Mixing string and number arguments doesn't work
new Date('2025', '1', '2') // Invalid Date or unexpected result

Why this exists: Historical API growth without consistent design principles.

How to avoid: Use one pattern consistently. I recommend either ISO strings or the numeric constructor with a helper function.

The Full Picture: My Original Error Explained

Let me trace through what happened with my original code:

debugging-my-error.js
// What I wrote:
const date = new Date(25, 2, 2)
// Step 1: Year 25 → 1900 + 25 = 1925
// Step 2: Month 2 → March (0=Jan, 1=Feb, 2=Mar)
// Step 3: Day 2 → March 2
// Result: March 2, 1925
// What I should have written:
const correct = new Date(2025, 1, 2)
// Or with string:
const correct2 = new Date('2025-02-02T00:00:00') // Local midnight

Best Practices Summary

After encountering these gotchas, here’s what I now follow:

best-practices.js
// 1. Always use 4-digit years
new Date(2025, 1, 2) // Not new Date(25, 1, 2)
// 2. Remember months are 0-11, or use a helper
function createDate(year, month, day) {
return new Date(year, month - 1, day)
}
// 3. Use ISO 8601 format for string parsing
new Date('2025-02-02T00:00:00') // Explicit local time
// 4. Be explicit about UTC vs local
// UTC methods: getUTCDate(), getUTCMonth(), getUTCFullYear()
// Local methods: getDate(), getMonth(), getFullYear()
// 5. For server-side code, prefer UTC
const utc = new Date('2025-02-02') // Date-only ISO = UTC

The Real Solution: Use a Date Library

The JavaScript Date object has been problematic since 1995. The Temporal API proposal has been in development for 9 years specifically to fix these issues. Until it’s widely available, I recommend using a library:

alternatives.js
// date-fns - modular, tree-shakeable
import { format, parseISO } from 'date-fns'
const date = parseISO('2025-02-02')
// dayjs - lightweight, moment.js compatible
import dayjs from 'dayjs'
const date = dayjs('2025-02-02')
// luxon - modern, timezone-aware
import { DateTime } from 'luxon'
const date = DateTime.fromISO('2025-02-02')
// Temporal API (coming soon)
const date = Temporal.PlainDate.from('2025-02-02')

These libraries handle the edge cases, timezones, and formatting that trip up native Date usage.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments