This blog was originally published at Leapfrog Technology.
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
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
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
Basically, if the string you are trying to parse is not in the format given by
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.
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).
Here is a quick run down of the possibilities:
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.
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
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
// 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.
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!)
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:
Is that ISO 8601? Almost, but no. There is no timezone part in that string. So this also falls into the ambiguous group.
new Date('2018-04-16T00:00:00') // Mon Apr 16 2018 00:00:00 GMT+0545 (Nepal Time)
Parsed as local time.
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.
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?
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.