Hitchhikers guide to using 'Date' in JS
The JavaScript Date API is a nice example of how taxing "We don't deprecate, ever" can get. The Date constructor has six completely different behaviors depending on what you pass it. Not variants. Different behaviors.
// Current time
new Date()
// Milliseconds since epoch
new Date(1736533800000)
// String parsing
new Date('2023-11-24')
// Year, month, day
new Date(2023, 10, 24)
// Year, month, day, hour, minute
new Date(2023, 10, 24, 21, 30)
// Year, month, day, hour, minute, second, ms
new Date(2023, 10, 24, 21, 30, 45, 500)If you give it an ISO date without a time (just YYYY-MM-DD), it assumes UTC midnight. But if you add a time without the Z, some browsers treat it as local, others as UTC. This isn't theoretical. Chrome and Firefox have behaved differently on this exact case.
The Z matters. Always add it if you want UTC.
// parsing date string
new Date('2023-11-24') // ISO date (no time)
new Date('2023-11-24T21:30') // ISO date with time (no timezone)
new Date('2023-11-24T21:30Z') // ISO date with time (UTC)
new Date('2023-11-24T21:30+05:30') // ISO date with timezone offsetRemember, when using Date constructor with strings, only ISO 8601 format is reliable.
When you pass numbers to the constructor, things get strange fast.
new Date(2023, 10, 24) // November 24, 2023Wait. November? We passed 10. Why November?
Because months are zero-indexed. January is 0, December is 11. This comes from C's tm struct from 1972. Java copied it. JavaScript copied Java. Now we're stuck with it. Does that mean 24 is also actually 25? Well...no, days are numbered from 1 to 31.
One more thing:
new Date(2023) // Not year 2023!
new Date(2023, 0) // This is year 2023A single number isn't a year. It's milliseconds since January 1, 1970. So new Date(2023) is 2.023 seconds after the epoch, which is January 1, 1970, 00:00:02.023. So, If you want year 2023, you need at least two arguments: year and month.
But wait, there's more:
new Date(99, 0, 1) // January 1, 1999
new Date(100, 0, 1) // January 1, 100 AD
new Date(2023, 0, 1) // January 1, 2023Years 0-99 get treated as 1900+n. This is Y2K panic code. Someone was worried about legacy code passing two-digit years, so they added this special case. Year 99 becomes 1999. Year 100 is literal.
Remember when using Date constructor with numbers, 0-99 for years means 1900-1999; months are zero indexed from 0-11; days start from 1-31, will wrap to next month if they overflow; only passing single number argument isn't the year, it's epoch.
Guess the Output: A Simple Algorithm
Here's how you would do to predict what the Date constructor will do:
Step 1: Count the arguments
0 arguments? Current time.
1 argument and it's a
number? Milliseconds since epoch. Not a year.1 argument and it's a
string? Parse as date string.ISO 8601is safe. Anything else is a gamble.2+ arguments?
year, month (0-indexed), day, hour, minute, second, millisecondin that order. Everything is in local time.
Step 2: For 2+ args, Check for two-digit years
If first argument is between 0-99, add 1900. Year 25 becomes 1925.
If first argument is 100+, it's the literal year.
Step 3: Month wrapping
Month argument is 0-indexed. 0 = January, 11 = December.
Month 12? Wraps to January of next year, same with larger values. So,
new Date(2023, 20)is01 Sep 2026.Similarly, Month -1? Wraps to December of previous year.
Step 4: Day wrapping
Days range from 1 to
<max_for_the_month>.Any numbers outside of the range, are wrapped to next/previous month. So,
new Date(2023, 2, 32)(allegedly 32nd march) becomes01 April 2023. Same with negative numbers.
Step 5: Timezone
String with Z? UTC.
String with timezone offset like +05:30? Parses offset but doesn't store it.
String without timezone? Pray.
Number constructor (2+ args)? Always local time.
Step 6: Invalid input
String that doesn't parse?
Invalid Dateobject. No error thrown.NaNin any argument?Invalid Date.undefinedin any argument? Treated asNaN, soInvalid Date.