This blog was originally published at Leapfrog Technology.
There have been some gotchas with working with Date in JavaScript that we’ve had to learn the hard way around. I’m hoping to blow your brains in a controlled manner now instead of having it blown later by a nasty bug.
Initializing Date with a string is ambiguous.
Let’s start with an example. Open up the console on Chrome and run this:
new Date('2018-3-14')
// Wed Mar 14 2018 00:00:00 GMT+0545 (Nepal Time)
Nice, it worked. Now, do the same on Safari (or just trust me if you don’t have access to Safari right now):
new Date('2018-3-14')
// Invalid Date
Wait, what!?
The ECMAScript Specification says that calling the Date constructor with a single string argument will create a new Date with the same implementation as that of Date.prototype.parse
.
It then goes on to say that Date.prototype.parse
will be implementation dependent if the string is not something that can be generated by Date.prototype.toString
or Date.prototype.toUTCString
.
Basically, if the string you are trying to parse is not in the format given by Date.toString()
or Date.toUTCString()
, you are screwed.
Chrome just wants to be extra and goes out of its way to support more formats; but what it really does is give the developers a false sense of security that their code works. This has given us enough “b..but… it works on my machine” situations.
Fine, then what formats does new Date() properly support?
That… also depends on what browser you are on. Here is a quote from the specifications:
The contents of the string are implementation dependent, but are intended to represent the Date in a convenient, human-readable form…
Luckily in this case, there is a consensus on using the ISO 8601 date format. It is quite simple and you probably are already using it:
2018–06–17 // Notice it's 06 not 6
2018–06–17T07:11:54+00:00
2018–06–17T07:11:54Z
20180617T071154Z
Lesson 1: Always use ISO 8601 date-time format, everywhere, always. Seriously.
Update
There has been update to the specifications since ES5 that defines ISO 8601 into the JavaScript specs itself and it is no longer just a consensus.
The whole timezone issue.
The Date object in JavaScript internally is just a number storing the number of milliseconds since 01 January, 1970 UTC.
JavaScript Date has a very rudimentary understanding of timezones and day light saving. It kinda knows what the timezone offset of the machine it is running on is and if DST is applied right now (for both of which it relies on the browser, which relies on the OS).
It does not have the capability of figuring out what time it is in different timezones or what timezone a particular Date object is pegged to. Infact, there is no way to peg a Date object to a particular timezone, all the operations on the Date object is based on the local timezone of the system it is running on.
Everything a Date object does is on that internal number of milliseconds it has in each Date object. So the only real influence of timezones is only when we are initializing that internal number.
For example, when you say new Date('2018-04-14')
what is the date object supposed to understand? That could be 1520985600000
if that date is in UTC or 1520964900000
if the date is in +05:45 (Nepal Time).
Knowing when JavaScript understands what is crucial to figuring out the timezone issue.
Here is a quick run down of the possibilities:
Date initialized with ISO 8601 date-time string.
const d = new Date('2018-04-14');
d.toUTCString();
// "Sat, 14 Apr 2018 00:00:00 GMT"
d.toString();
// "Sat Apr 14 2018 05:45:00 GMT+0545"
This is the largest culprit to most of the datetime related issues. Consider you take this Date object and do a getDate()
on it. What would be the result? 14, right?
d.getDate();
// 14
Here’s the catch: look at the time part in the output of d.toString()
above. Since the Date object only works with the timezone of the local system, everything it does on the Date object is based on the local timezone.
What if we ran the same code on a computer in New York?
const d = new Date('2018-04-14');
d.toUTCString();
// "Sat, 14 Apr 2018 00:00:00 GMT"
d.toString();
// "Fri Apr 13 2018 14:15:00 GMT-0400"
And, what date is it?
d.getDate();
// 13
Come to think of it, this is obvious. 2018–04–14 00:00
in London is 2018–04–14 05:14
in Nepal and 2018–04–13 14:15
in New York.
As it turns out, 2018-04-14
was just a short hand for 2018-04-14T00:00:00Z
. See the Z
at the end? That means that the given date-time is in UTC.
The results are different if we get rid of the Z.
const d = new Date('2018-04-14T00:00:00+05:45');
d.toUTCString();
// "Fri, 13 Apr 2018 18:15:00 GMT"
Which is true, the midnight of April 14 in Nepal is 18:15 of April 13 in London. Still, d.getDate()
will give 14 in Nepal, but 13 anywhere west of Nepal.
Lesson 2: Date initialised from ISO 8601 date-time strings are parsed according to the timezone information on the string, but it is initialized into local time.
Date not initialised from strings.
new Date(2018, 3, 14, 0, 0, 0, 0);
Guess what date that is. March 14, 2018? Wrong. That is April 14, 2018. You see, months start from 0
in JavaScript world. But days still start from 1
. Don’t ask me why.
But the nice thing is, that is April 14, 2018 in every computer in every part of the world.
When you initialise the Date object directly with the arguments it is always considered that it is in local timezone.
This is your solution for things like birthdays that is just a date and don’t care what timezone it is initialised in. For most other things, if it matters when and where something happened exactly, it might be best to stick with ISO 8601.
But what if you have a date-time that needs to be initialized from UTC? Turn it into an ISO 8601 string? Maybe…, or just use Date.UTC
.
// These two are equivalent:
const a = new Date('2018-04-16');
const b = new Date(Date.UTC(2018, 3, 16));
a.toString() === b.toString();
// true
Lesson 3: If you are paranoid or just work with local times, always initialize Date directly with the arguments.
Strings that are not ISO 8601.
As mentioned before, strings that do not confirm to ISO 8601 format are parsed ambiguously between browsers. But the most common implementations are worth discussing.
Chrome supports many kind of formats (it might be worth noting that Node uses the same V8 engine as Chrome so the results are the same):
new Date('April 13') // April 13 2001 Local timezone
new Date('5/13/2012') // May 13 2012 Local timezone
new Date('15/12/2009') // Invalid Date (Finally!)
On Firefox:
new Date('April 13') // Invalid Date
new Date('5/13/2012') // May 13 2012 Local timezone
new Date('15/12/2009') // Invalid Date
Firefox seems to be a bit more strict, but Safari is by far the strictest.
The thing to note here is that all of these are in local timezone, as if they were initialized directly from the arguments.
But there is an exception to that as well. Consider this:
new Date('2018-04-16T00:00:00')
Is that ISO 8601? Almost, but no. There is no timezone part in that string. So this also falls into the ambiguous group.
On Chrome:
new Date('2018-04-16T00:00:00')
// Mon Apr 16 2018 00:00:00 GMT+0545 (Nepal Time)
Parsed as local time.
On Safari:
new Date('2018-04-16T00:00:00')
// Mon Apr 16 2018 05:45:00 GMT+0545 (+0545)
Parsed as UTC.
This can cause a lot of confusion and headache if you’re only testing on Chrome.
I reiterate, only use ISO 8601 format date strings. Always.
Update
The specification for ES5 states that ISO 8601 string without a timezone part should be treated as UTC, but specs for ES6 states that they should be treated as local time. Safari is just slower in implementing the specifications.
NVM, I’ll just use moment.
First, moment is not a silver bullet against every JavaScript Date issue. Second, many of the caveats in the internal Date object still apply to moment.
For example, both of these will give you Invalid Date in Safari, but will work fine on Chrome:
new Date('2018-3-14')
// Invalid Date
moment('2018-3-14')
// Invalid Date
Also, I’ve seen project that have more than half of their bundle size for moment. That may not be something you care about in the beginning but it’s surely coming to bite you in the future, and it might be too late to turn back by then.
I have nothing against moment, I heavily use it — but on the backend without size constraint; I still haven’t found a convincing use case to use it on the frontend. Maybe DateFNS will suffice your use case?
To Conclude.
Bugs related to incorrect assumptions about the Date object are common and everyone will eventually face it. Gaining a better undestanding of how things work underneath and establishing and enforcing best practices are may be the only way around these. We’ve had our share of combing through code and hunting down bugs to find a faulty Date object underneath it all.
Top comments (3)
Your Moment example is an example of what NOT to do with Moment. Moment only reliably parses ISO 8601 formats. Any other format behaves inconsistent and therefore should be given the format being use during the parsing. That is outlined in their docs momentjs.com/docs/#/parsing/string.
moment('2018-3-14', 'YYYY-M-DD') produces a valid date.
True. I just wanted to point out that you need to know what you are doing (while using moment or native dates) and just using moment blindly does not solve everything.
An excellent and complete summary of what to expect. I've also went through all the layers of Date string happiness and this seems to cover it all! Great article, thanks for the write-up.