Let’s start with a brief overview of what it means for something to happen in the universe.
> new Date("2020-01-08T19:47:00.000Z") Wed Jan 08 2020 14:47:00 GMT-0500 (Eastern Standard Time) > moment("2020-01-08T19:47:00.000Z").format("h:mm a MMM DD, YYYY") // using moment.js "2:47 pm Jan 08, 2020" > format(parseISO("2020-01-08T19:47:00.000Z"), "h:mm a MMM dd, yyyy") // using date-fns "2:47 PM Jan 08, 2020"
If we change the timezone setting on our computer to Oslo, we get a different result:
> new Date("2020-01-08T19:47:00.000Z") Wed Jan 08 2020 20:47:00 GMT+0100 (Central European Standard Time) > moment("2020-01-08T19:47:00.000Z").format("h:mm a MMM DD, YYYY") // using moment.js "8:47 pm Jan 08, 2020" > format(parseISO("2020-01-08T19:47:00.000Z"), "h:mm a MMM dd, yyyy") // using date-fns "8:47 PM Jan 08, 2020"
Date is always stored as UTC, and most of the native methods automatically localize the result.
There are a few notable exceptions to the rule of date methods automatically localizing. First, the
getTime() method returns the closest thing to the true underlying representation of the date in memory: the number of milliseconds since 1970, UTC time. Likewise,
toISOString() always returns the date in UTC time, which you can tell because the string ends in the letter Z, indicating UTC time for historical reasons related to the British Empire (and are beyond the scope of this article).
But, of course, it isn’t. We do not always want to, and do not always have to, think about dates as occurring at a moment in time.
Not everything with a date actually happened at a moment in time in the universe. When is Valentine’s Day? There is no moment in the universe when Valentine’s Day starts. It starts at different (universal) times for people in different timezones. It’s enough to say that it starts at 12am on February 14.
Pure dates – holidays, insurance coverage periods, etc – are the clearest example of dates that resist localization.
Here’s another example: what if I were to say that I think 4:30 is too early a time to get up in the morning? I’m talking about the canonical ideal of 4:30, and it would be inappropriate for that value to change onscreen if you were to change the timezone on your computer.
It’s not too difficult to store this canonical ideal in a data structure that resists localization:
> "Steve thinks 4:30 is too early" "Steve thinks 4:30 is too early" > moment.localizeThisSomehow("Steve thinks 4:30 is too early") Uncaught TypeError: moment.localizeThisSomehow is not a function
Here enters our old friend
string, which is the time-honored alternative for storing dates while avoiding UTC and time zone concerns. Not that strings are a perfect choice for representing “wall time”, especially if we have to run some date calculations down the road, such as determining the earliest too-early time among a group of people.
console.log(new Date("2020-01-01").toString()); // -> Tue Dec 31 2019 19:00:00 GMT-0500 (Eastern Standard Time) console.log(format(new Date("2020-01-01"), "MMMM do yyyy")); // -> December 31st 2019 // moment magically fixes this problem for you console.log(moment("2020-01-01").format("MMMM Do YYYY")); // -> January 1st 2020 // ... unless you invoke with a date and not a string console.log(moment(new Date("2020-01-01")).format("MMMM Do YYYY")); // -> December 31st 2019
What’s going on here?
console.log(new Date("2020-01-01").toISOString()); // -> 2020-01-01T00:00:00.000Z console.log(new Date("2020-01-01").toString()); // -> Tue Dec 31 2019 19:00:00 GMT-0500 (Eastern Standard Time)
Most databases have a rich set of alternatives for storing dates:
timestamp with timezone, and
timestamp without timezone. They can even be stored as
Date by the time we want to work with them. Therefore, it’s important to understand the assumptions that have been built into this conversion along the way.
For example, let’s say that we’re running Node.js on the server, connecting to a PostgreSQL database via the
pg library, and querying a PostgreSQL
Date always represents a particular moment in time, so our life is about to get unpleasant.
In this case, the
pg library will return the moment that midnight struck on the date in question using the timezone that the server was set to. This is not necessarily the timezone of the user, nor the time that the event actually happened. In fact it’s likely that this value does not represent a real event that happened at all, given that it was being stored as a
date in the database and not a
Date that does not really represent what we want it to represent. However, some pieces of it represent what we want to render, and we need to tease out and massage the pieces we want.
Let’s say you’re a front-end developer. You’re just looking to render “February 14”, and have gotten bug reports that users in other time zones are seeing “February 13”. When you dig a bit into this object you see that even the when you’re correctly rendering it as “February 14” you’re getting lucky, because the underlying time is sometimes some number of hours off of midnight. You now have the distinct impression that your day is about to be ruined.
Take a deep breath and think through the following principles:
Dateobject often supplies too much or erroneous information
So what’s going on here? The date you’re receiving from the server is a moment in time. It’s midnight of the timezone to which the server is set. Your code is probably localizing it per the timezone settings on the user’s computer. If you’re just rendering the day, month, and year, you’ve got about a 50% chance of getting it right.
You can manage the user timezone localization, because this is just implemented by some of the native
Date methods. Notably,
toISOString() will always return the same result no matter what the user timezone is. If this is the sort of date that you want not to be localized, then that’s a good place to start.
Resist the temptation to assume that you know the server’s timezone! Not only would that solution not be portable, but in the United States your server’s timezone is going to change twice a year, when daylight savings time hits. Rather, convince the back-end devs to add an endpoint to show the timezone of the server.
const localOffset = new Date().getTimezoneOffset(); // in minutes // -> 480 for California in the winter
getTime() converts the date to an integer representing milliseconds. If we back out the server timezone offset, and apply the local timezone offset, we will have shifted the universal time from the server’s midnight to the user’s local midnight.
const dateFromServer = new Date("2010-02-14T08:00:00.000Z"); // California midnight const serverOffset = 480; // in minutes, from that API call const serverOffsetMillis = 60 * 1000 * serverOffset; const localOffset = new Date().getTimezoneOffset(); // in minutes const localOffsetMillis = 60 * 1000 * localOffset; const localMidnight = new Date(dateFromServer.getTime() - serverOffsetMillis + localOffsetMillis); console.log(localMidnight.toString()); // -> Sun Feb 14 2010 00:00:00 GMT-0500 (Eastern Standard Time)
Adapted from my NorfolkJS talk