Ursa Health has the ideal solution for participants in CMS's Primary Cares Initiative. FIND OUT MORE 

February 26, 2020 . Steve Hackbarth

Working with dates and timezones in JavaScript: a survival guide

Ursa Health Blog

Visit Ursa Health's blog to gain healthcare data analytics insights or to get to know us a little better. Read more...

Many JavaScript developers approach dealing with dates and timezone issues with apprehension. And indeed, it’s easy to get turned in a knot as you’re trying to get times working sensibly for all your users. Nor are date and timezone problems the sort of issue that a great library can make easy. But with a solid understanding of the fundamentals, you’ll find that thinking through how to work with dates in JavaScript becomes a tractable problem.

Let’s start with a brief overview of what it means for something to happen in the universe.

Here’s something:

It happened at a particular moment in time, and we mostly want to know what time this was in our local timezone. JavaScript is excellent at dealing with this sort of representation of a date, whether we’re working on the browser or the server.

> 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"

This is as it should be. The tweet happened at a moment in time, and that moment should be localized for the user. The timezone that Dave Jorgenson happened to be in is not something we really care about, and the timezone of the Twitter data center is definitely not something we care about. JavaScript’s internal representation uses the “universal” UTC time but by the time the date/time is displayed, it has probably been localized per the timezone settings on the user’s computer.

And, indeed, that’s the way JavaScript is set up to work. The JavaScript 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).

Either way, the JavaScript date always represents a moment in time in the universe, such that it can be easily – and most of the time automatically – localized to the user’s environment. And if this is all we ever wanted, life would be great.

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.

Why resist localization?

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.

Now that we’ve established that there are some circumstances in which we do not want to automatically localize, let’s see how sensibly the JavaScript language approaches this problem.

Here’s something to ruin your day

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?

As we’ve covered, JavaScript stores dates universally, and localizes them for the user. When we created a new date at 2020-01-01, the universal date JavaScript picked was midnight in London, which is the night of New Year’s Eve in the United States, which explains the lamentable way it’s been localized.

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)

How to make JavaScript dates work for you

Most databases have a rich set of alternatives for storing dates: date, time, datetime, timestamp with timezone, and timestamp without timezone. They can even be stored as varchar or some string-like data type. No matter how they’re represented in the database, chances are they’ll have become a JavaScript 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.

Because JavaScript only has one maximalist data type to represent dates, much of the headache of working with dates and timezones in JavaScript revolves around dealing with JavaScript dates that only partially represent the information we were actually trying to encode. That information needs to be carefully extracted from the rest of the information that JavaScript insists on adding.

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 column. Remember that JavaScript 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 datetime/timestamp/etc.

Nevertheless, we’re stuck here with a JavaScript 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:

  • JavaScript always considers dates to be particular moments in time.
  • They’re stored as UTC but most methods automatically localize for you.
  • Not every date is really something that happened at a particular moment in time.
  • In these cases, the JavaScript Date object 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.

On the other hand, you cannot easily code your way out of the server timezone localization, because this has already been baked into the universal time of the JavaScript date. So you really do need to know the timezone of the server, which is work that needs to be done server-side.

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

To render this date with confidence we have to actually change the universal time that the JavaScript object is trying to represent. Remember that 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);
// -> Sun Feb 14 2010 00:00:00 GMT-0500 (Eastern Standard Time)

In the JavaScript community we hate writing six lines of code for something as basic as confidently rendering a calendar date. Both moment and date-fns have utility functions that can help a bit, but they can’t do this all for you. When you’re working with dates in JavaScript there’s still no substitute for understanding what’s going on under the hood.

Adapted from my NorfolkJS talk

Follow us

If you'd like, we'll let you know every time we post something new here, so you don't miss a thing. Just enter your subscriber information here, thanks!