But what happens if you are building a Todo app — you know, the most overused web application in existence, and you want to add calendar reminder functionality?
If your dates don’t handle timezones correctly and you end up with an “off by 1” error, your user who missed their job interview is going to be FURIOUS with your app.
It took me at least 3 years of programming before I finally sat down to understand this topic. And if I had to guess, a few of you reading this are in a similar spot, so cheers to that! 🍻
Before I start reading… What about MomentJS?
It seems like all the cool kids are using MomentJS these days (or if you’re even cooler — Luxon).
Finally, let’s begin.
Imagine this — you‘ve been coding for 8 hours, your brain is a bit tired, and you
console.log one of your dates during an intense debugging session:
new Date('2020-09-30')// Output: Tue Sep 29 2020 20:00:00 GMT-0400 (Eastern Daylight Time)
Just wait, it gets more confusing. Here’s my local computer (the computer I’m standing at right now in the Eastern part of the United States):
And here is the webserver that I run my golf training application on:
Notice anything? On my local computer, the result of
Tue Sep 29 2020, while on my server, the result of
Wed Sep 30 2020. And furthermore, both computers returned the same result for the
Either I’m going crazy, or we just typed in the exact same thing on two different computers and got a completely different answer — not to mention one of the computers got my date completely wrong!! Or did it…
If we’re going to learn this once and for all, we need to start by learning about a computer’s timezone.
Here’s my local computer’s timezone:
And here’s my server’s timezone:
UTC stands for “Coordinated Universal Time”, and per the name, it is the basis for how time is kept. Well… Kinda.
Before we had a record of time on our wrists, in our pockets, on our walls, and on our overpriced coffee makers, people needed a reliable way to calculate the current time. And how did they do that? The SUN.
Imagine sticking a perfectly vertical pole in the ground on a perfectly sunny day. When the sun rises, that pole is going to cast a shadow west, and conversely, will cast a shadow east as the sun sets. If you were to count how long it took for that shadow to move from point to point, you could (kind of) keep track of time.
But there’s a problem… If I put a vertical pole in the ground where I live in the U.S., the shadows aren’t going to be cast in the same direction at the same time as someone putting a pole in the ground somewhere in Ireland.
To solve this, we need a common reference point and a time standard. At some point, some scientific people thought that using 0° longitude (aka the “Prime Meridian”, which happens to be Greenwich UK) would be a good “universal reference point” to put that proverbial “pole in the ground”.
The next challenge is actually calculating “time” based on that “pole in the ground”. It used to be calculated using Greenwich Mean Time (The GMT Standard— started in 1847), but sometime in the early 1960’s, some engineers decided that we needed a new standard with a more consistent calculation than GMT.
In 1970, the UTC standard was finalized, but in 1972, an amendment was made to add “leap seconds” periodically in order to keep UTC in agreement with mean solar time (which is slightly imprecise due to the irregularity of Earth’s rotation).
Today, there are 3 main calculations of “time” (which is starting to mean less and less to me as I write this):
- Mean Solar Time (UT1)
- Universally Coordinated Time (UTC)
- International Atomic Time (TAI)
TAI is the most accurate and is calculated by Atomic Clocks.
UTC is derived from TAI, but has leap seconds added to it for the sake of staying synchronized with UT1.
The last leap second was added June 30th, 2015, and in 2023 at the World Radio Communication Conference, there will be a discussion about possibly removing leap seconds from the UTC time standard, which hasn’t been changed since 1972.
Everything revolves around UTC
Since 1972, the world has been operating based on this UTC standard, and it is considered +/- 0 when talking about timezones:
Notice the values at the bottom of the picture
If we move West of the UTC timezone, each successive timezone becomes 1 hour more negative, while the opposite applies while traveling East.
And as you might have guessed, since the world is a sphere, moving East eventually gets you to a “Western” timezone, so for example, the -10 timezone is equivalent to the +14 timezone.
Timezones can be defined in two ways (and what about Daylight Savings Time?)
We can talk about timezones in two ways:
- As a numerical reference to the “zero-point”, which is the UTC timezone
- As a commonly known name, such as “UTC”, or “EST”
The one thing that we HAVE to remember is that UTC does not change. EVER.
I live in the Eastern part of the U.S., so from Nov 4th, 2019 → Mar 7, 2020, I lived in the EST timezone or the “Eastern Standard Time (North America)” zone from the official timezone list. The EST timezone could also be referred to as “UTC-05” because it is 5 hours *West of *the UTC timezone.
But what happens on March 8th, 2020— the first day of “Daylight Savings Time” in the U.S.?
If UTC never changes, then how do we know it is Daylight Savings Time?
On March 8th, 2020 (first day of Daylight Savings Time in U.S.), my timezone changes from EST → EDT, or “Eastern Standard Time (North America)” → “Eastern Daylight Time (North America)”.
EDT can also be referred to as “UTC-04” because we have “sprung forward” as a result of daylight savings time, and now, I am only 4 hours behind UTC.
See how that works?? UTC never changes, but the timezone you are living in DOES. To most, this doesn’t matter because when I say “The event starts at 10 a.m. EST”, nobody is going to question me whether I am talking about EST or EDT. Nobody cares because our smartphones take care of the “behind the scenes” conversions.
In your brain, when you see “GMT”, just think “UTC”.
And when you see “GMT-0400”, this is the same thing as saying “UTC-04”, “EDT”, or “Eastern Daylight Time (North America)” as we defined in the previous section.
But what about the UNIX Epoch?
So let’s get back to computers. You’ve certainly heard the term, “UNIX Epoch”, which is defined as January 1st, 1970.
Here it is in ISO 8601 format (more on this later):
While the “time” engineers were hard at work defining the UTC standard in the early 1970s, computer engineers were also hard at work building computers. When you build a computer, you have to give it a reference point and a “tick” frequency in order for it to remember what time it is. And that reference point was arbitrarily chosen as midnight, January 1st, 1970.
Several sources debate the true reason behind this date, but in the end, it doesn’t matter. It is simply a reference point. Feel free to leave your theories (or stories) about this in the comments — I’d love to read them.
Hmm… That date sounds familiar…
const myDate = new Date('2020-01-01');console.log(myDate.valueOf()) // 1577836800000
valueOf() method tells us that midnight on January 1st, 2020 (UTC timezone) is
1577836800000 milliseconds after midnight on January 1st, 1970 (UTC timezone).
And if you’re like me, you are probably wondering about the **.032876712 **part. This represents leap years. Since 1970, we’ve had leap years in the following years:
1972, 1976, 1980, 1984, 1988, 1992, 1996, 2000, 2004, 2008, 2012, 2016, 2020
Since we are using January 1st, 2020, and the extra day gets added in February, we need to exclude the 2020 leap year in the calc, which leaves us with 12 total leap years since Jan 1st, 1970. And if we convert years to days:
And that’s how we get those extra decimals 😆
So if we wanted to calculate a date before January 1st, 1970, is that possible?
Of course! The value will just be negative.
const d = new Date('1912-06-23');console.log(d.valueOf()) // -1815350400000
Possible Date Object Output Values
When we printed the
valueOf() the date above, you might have noticed that the output was a number. This is the first of 3 possible outputs of a Date object:
- Primitive Value of Date Object (number)
- ISO 8601 (string)
Here are examples of each:
We’ll talk a little more about this format later, but for now, just remember that the
T is the indicator for time, while the
Z is the indicator of timezone. In this case, “Z” comes from “Zulu Time”, which is a military time zone reference for the UTC timezone. So remember:
Every time you use the
A Tale of Two Computers (and why they can’t seem to agree on what date it is!)
In this section, we are going to be using two computers and a single date (Sep 30, 2020 — the day of writing) to demonstrate exactly why your date is “off by 1”.
Here are two different computers with different timezones as shown in the screenshot below. Notice how the timezones printed look a bit different than the timezones we talked about earlier. That is because there are two official timezone lists — a “regular” one and a “computer” one. In the first part of the post, we used the “regular” list, while below, we are using the “computer” list. There is no real difference, just semantics.
Local Computer (left), Webserver (right)
So remember as we go through these examples:
These two computers have different timezones
Let’s start with a simple date and getting the primitive value of it.
Remember, this is the number of milliseconds since midnight, Jan 1st, 1970
So far, so good. We passed the value
'2020-09-30' as an input, and got the same output —
Now, let’s see what happens when we print this as UTC time:
Still looking good here. As expected, they are showing the same values. Let’s look at this as an ISO string, which should be the same thing as a UTC string.
And yet again, we get the same value, as expected. But now it’s time for the real fun:
Let’s think through this… We know the following things:
- We used an input value of
1601424000000milliseconds since midnight, Jan 1st, 1970 (UTC time)
- If we convert the primitive value to UTC time, it represents midnight, Sep 30th, 2020.
Remember, the computer on the left is in the EDT timezone, which is “UTC-04”.
So if the internal value of this date is
160142400000(midnight, Sep 30th, 2020 UTC time), then if we convert this value to EDT time, we have to subtract 4 hours. If we subtract 4 hours starting at midnight, then the “relative” timezone value of this date is 8 p.m. (i.e. 20:00), Sep 29th, 2020.
- You enter a value such as
toString()method or when you
2020-09-30, you weren’t specific enough.
So let’s talk about how we can avoid this problem.
- What was my input value, and did I specify a timezone? (i.e.
- What is the UTC value of my date? (you can always find this using
Let’s say that you are building an app that allows your user to specify their birthday in their user profile. And imagine that our user’s birthday is on June 28th, 1971, and they live in Florida (Eastern time zone).
So you code up an app real quick, and ask the user for his/her birthday:
The incorrect way to show a date in JS
The user has selected June 28th, but your web app is showing June 27th when printed as a string 😢
Here’s the code for that web app — let’s see if you can spot the mistake:
Let’s go through our checklist from earlier:
- What is the input value, and did I specify a timezone? —By default, the HTML5 input is going to record the date selection in UTC. So technically, we are specifying a timezone, but it is the default — UTC.
- What is the UTC value of this date? — If you were to print
console.log(dateValue.toISOString()), you would get
1971-06-28T00:00:00.000Z, which represents the UTC value.
getFullYear()methods, these will convert the raw date into the user’s local timezone, which is the Eastern Standard Timezone (North America)!
So how do we fix it?
There are two methods:
- Easiest — Display the date in UTC time (consistent with how it was input)
- Harder — Ask the user their timezone, and specify that timezone when you save the date
With the first method, all you need to do is change this:
const day = dateValue.getDate();const month = dateValue.getMonth() + 1; // Return Value is 0 indexedconst year = dateValue.getFullYear();
const day = dateValue.getUTCDate();const month = dateValue.getUTCMonth() + 1; // Return Value is 0 indexedconst year = dateValue.getUTCFullYear();
And after these changes, voila! It works!
Correct Date Example
The second method is more complicated, but could still work. Let’s say that you asked this user to select their time zone, and they selected the “EDT — Eastern Daylight Time (North America)” time zone.
As you can see, this is many more lines of code, and a bit repetitive — definitely not recommended.
The example in the previous section highlights a specific “gotcha” scenario, but there are many other ways to work with JS dates.
For example, you can pass pretty much anything into the Date object:
// EXAMPLE #1// Inputs as arguments// Date(year, month, day, hour, minute, second, millisecond)// Note: the month is 0-indexed (I have no clue why...)new Date(2020, 11, 2, 7, 10);// EXAMPLE #2// Inputs as various strings// This works with pretty much anything you can think ofnew Date('Jan 20 2020');new Date('January 20 2020');new Date('Jan-20-2020');new Date('Jan 20 2020 02:20:10')// EXAMPLE #3// Inputs as numbers (milliseconds)new Date(102031203)// EXAMPLE #4// Inputs as ISO 8601 (we are about to talk about this)new Date('2020-01-20T00:00Z')// EXAMPLE #5// Inputs with timezone specificationsnew Date('Jan 20 2020 02:20:10 -10:00') // SPECIAL CASEnew Date('Jan 20 2020 02:20:10 -1000') // SPECIAL CASEnew Date('Jan 20 2020 02:20:10 (EDT)') // SPECIAL CASE// EXAMPLE #6// The current moment, specified in the user's local timezonenew Date(Date.now()) // SPECIAL CASE
Remember, always pay attention to your input values! In examples 1–4, we are specifying these dates in UTC time! Examples 5 and 6 are the only cases where we are specifying the dates according to a specific timezone. In example 5, we are doing so explicitly. In example 6, the
Date.now() static method will automatically calculate the current date and time in your computer’s local timezone. This method returns a native Date value — the number of milliseconds since midnight, Jan 1st 1970 UTC, so we need to pass that into the
Date() constructor to get an actual date object.
In other words, if you are planning a Livestream event that is happening in California on August 10, 2020 (remember, this is during Daylight Savings Time) at 8 pm, then you need to input it as (where PDT is Pacific Daylight Time):
const myEventDate = new Date('Aug 10 2020 08:00:00 PDT');
and display it as:
This will ensure that whoever reads this from their personal computer (in a different timezone) will have this Livestream event time and date displayed in their local time. Feel free to try it yourself! The output of
myEventDate.toString() should print the time and date of the event converted to YOUR timezone.
There has got to be an easier way to do this (MomentJS and LuxonJS)
Here’s that same problem from earlier demonstrated with MomentJS. And remember:
Not only does Moment prevent the common Date pitfalls (in most cases), it also provides easy methods for formatting dates!
Appendix: A Primer on ISO 8601 (optional)
- Primitive Value of Date Object (number)
- ISO 8601 (string)
In this appendix, we’ll dive deeper into the ISO 8601 standard format.
ISO 8601 is an international standard, and comes in two main variations:
The only difference between “Basic” and “Extended” variations of ISO 8601 is the use of delimiters. Here is the same date in “Extended” format:
Here are the pieces:
Most of this is self-explanatory.
YYYY represents the full year,
MM represents the month padded with zeroes if necessary (i.e.
01 for January), and
DD represents the day padded with zeroes if necessary.
T is simply an indicator that “we are about to enter time values rather than date values”.
SSS represent hours, minutes, seconds, and milliseconds respectively.
Z represents the UTC time zone. Anytime you add that
Z to the end of your ISO date string, your date will be interpreted as UTC time. If you leave it off, the date will be interpreted as local time.
The last thing you need to know about ISO 8601 strings is that they are flexible. You can reduce specificity with them. For example, all of the following ISO strings are valid:
new Date('2020Z') // 2020-01-01T00:00:00.000Znew Date('2020-05Z') // 2020-05-01T00:00:00.000Znew Date('2020-05-10T00:00Z') // 2020-05-10T00:00:00.000Z
And if we drop that
Z, the dates are still valid, but they are interpreted as local time (if you don’t understand this now, that’s okay as we’ll cover it more later)
// Adding the Z interprets dates as UTC timenew Date('2020-05-10T00:00Z') // 2020-05-10T00:00:00.000Z// Dropping the Z interprets dates as local timenew Date('2020-05-10T00:00') // 2020-05-10T04:00:00.000Z
4000 words later…