Understanding Javascript Dates and Web APIs

ยท

5 min read

The Story

Your client has a form in their web application where they want to specify a datewhich will get submitted to an API and stored along with the rest of the form information. The web application is used all around the globe by users in different time zones. The client requires that when the date is stored, it displays the same for all users. So if a user sets a date of March 3rd while they are in the Eastern time zone, then when another user views the site anywhere from the Pacific to the other side of the planet, then they should see March 3rd also.

The Problem

You retreive the date from the API, create a Javascript Date object with new Date(theDate), display it to the user in the browser, and discover that the date is showing a different day then what the API gave you. The browser shows that the date is for the previous day. ๐Ÿ˜•

Why is this happening?

When an API provides you with a date, it is commonly (and should be) in a universal standard ISO 8601 string format like YYYY-MM-DD (ex: 2020-03-03) which is easily usable across many platforms. However, when you create a new Javascipt Date object from this string, you end up with a different date if you are in a time zone with a negative offset (like anywhere from Portugal to the United States Minor Outlying Islands). For example, if you are in the Eastern time zone and create a Javascript Date object like new Date('2020-03-03'), then your browser shows it as Mar 2, 2020 19:00:00 (i.e. 7PM the previous day).

Javascript doesn't have a Date only object. All Javascript Date objects include a time. When you instantiate a new Date object with only a date like new Date('2020-03-03'), then it will automatically assume that the time is 00:00:00. To make matters a little more tricky, it will assume that the entire date/time has a time zone offset of 0 because an offset wasn't specified in the string when you created the object. So the Date object will represent a full date/time of 2020-03-03 00:00:00 GMT time.

When your browser displays this object, it will apply the machine's local timezone offset to it. If you are in the Eastern time zone (UTC-5), then 5 hours will be subtracted from the date whenever you present it in a human readable format via methods like toString() or toLocalDateString(). So 2020-03-03 00:00:00 GMT becomes 2020-03-02 19:00:00 EST - notice the date part is now in the previous day.

The Solution

You could simply just display the string to the user as you received it from the API, but this is not an attractive format to users who are used to seeing dates presented in different formats like 3/3/21 or Mar 3, 2021. You probably need to convert this string in to a Javascript Date anyhow for some reason like to supply it to a datepicker component which should show the same date as what was retrieved from the API. To do this, you just need to do a little post-processing of the date object once you instantiate it like so:

var theDateObject = new Date(theDateStringFromApi);
theDateObject.setMinutes(theDateObject.getMinutes() + theDateObject.getTimezoneOffset());

What the above code will do is adjust the time according to your local offset. Then when you execute toString(), the date will match exactly what you received from the API and subsequently used to initialize your date object.

var d = new Date('2020-03-03'); 
d.toDateString(); // 'Mon Mar 02 2020' - notice date doesn't match what was passed to constructor
d.setMinutes(d.getMinutes() + d.getTimezoneOffset());
d.toDateString(); // 'Tue Mar 03 2020' - now date matches what was passed to constructor

What about posting dates to APIs?

Posting isn't as much of an issue when you are only posting the date portion to an API. You'll typically do something like this:

  1. Create your Javascript Date using your local date/time
  2. Use your favorite date formatting tool to format the date to YYYY-MM-DD format
  3. Post the formatted date string to the API

Your format tool will typically format based on your local date. It doesn't do any conversion like .toISOString() to convert it to an ISO string beforehand. However, if your API does require a date and time be posted, it likely will expect them to be in ISO format and you may have to make some adjustments before posting.

Apollo Client and GraphQL

Apollo Client and GraphQL are quite common nowadays - unless you're reading this far in the future in which case they may be obsolete by now (hello from the past btw). Depending on how you have your API defined, you may be able to pass your date object directly to the GraphQL API via Apollo Client - or so it would appear.

The reality is that your date object gets converted to an ISO datetime string with toJSON() or toISOString() before sending. This causes the time (and possibly date) to be shifted according to your local offset.

For example, if you are in Eastern time again (UTC-5) on March 3, 2020 and create a new date at 11 PM (23:00) and pass this date object as a variable to your mutation, then behind the scenes it is converted to the string 2020-03-04T04:00:00.000Z and that is what is sent to the API. The API receives this and stores it thinking it is in UTC and has no way of knowing that you sent it from a +9:30 offset. In the end, the date that it persists isn't what you thought would be stored (i.e. it is not the date that you created originally).

To accommodate for this, you need to do like we did above and adjust the time of the date object before you send it, but this time we subtract the minutes according to the offset.

var theDateObject = new Date();
theDateObject.setMinutes(theDateObject.getMinutes() + theDateObject.getTimezoneOffset());
var d = new Date(2020,2,3,23,0,0);  // March 3, 2020 11 PM
d.toJSON(); // '2020-03-04T04:00:00.000Z' - date is for the next day
d.setMinutes(theDateObject.getMinutes() - theDateObject.getTimezoneOffset());
d.toJSON(): // '2020-03-03T23:00:00.000Z' - date now matches

Once you do this, you can supply this date object directly to your mutation variable and when it converts it to ISO/JSON format, then the resulting date/time sent will match exactly with the date/time you used to create the object.

ย