What I learned about JS Date from writing a simple date library
The built-in Date object is notoriously difficult to use. It is why there are several date libraries out there, such as Day.js, luxon, date-fns, etc. Recently, I've implemented a date library that acts as a light abstraction layer, providing syntactic sugar like the date libraries mentioned above. I won't go into detail about why I didn't just use one of those libraries in this article, and I will save that for another time. Instead, I want to discuss my learnings and recommendation after learnings from creating this date library.
Best way to create a new date
As you can see below, there are many different ways to create a new Date object.
const today = new Date(); const birthday = new Date("December 17, 1995 03:24:00"); // DISCOURAGED: may not work in all runtimes const birthday2 = new Date("1995-12-17T03:24:00"); // This is ISO8601-compliant and will work reliably const birthday3 = new Date(1995, 11, 17); // the month is 0-indexed const birthday4 = new Date(1995, 11, 17, 3, 24, 0); const birthday5 = new Date(628021800000); // passing epoch timestamp // Borrowed from MDN docs
After some struggles, I found the best way to create a Date object is using the ISO8601-compliant method.
Why ISO8601-compliant method?
I find this method the most straightforward to work with:
- It follows a strict format of
YYYY-MM-DDTHH:mm:ss.sssZ, so there is no confusion around the date format, making it easy to read.
- It is still flexible, so we can still create dates without specific data, e.g.
new Date("1995-12-17")or with the hour and minutes
new Date ("1992-12-17T12:50").
- We can easily convert a date into UTC by adding
However, I also noticed a few disadvantages of using this method.
- First, it is not as flexible as creating a Date with integers, which allows parsing partial time formats. When passed just the hour,
new Date(1992, 11, 17, 12) // "1992-12-17T12:00:00.000Z" new Date('1992-12-17T12') // null
- Second, it cannot infer an unknown date format. In the following example, it didn't know what to do when I passed 50 as the Date.
new Date(2000, 0, 50) // "2000-02-19T00:00:00.000Z" new Date('2000-01-50') // null
Avoid parsing integers
Although this method is the most flexible when it comes to creating new dates. Here's a list of examples:
new Date(2022, 10, 27) // "2022-11-27T00:00:00.000Z" new Date(2022, 10, 27, 15) // "2022-11-27T15:00:00.000Z" new Date(2022, 10, 27, 15, 40) // "2022-11-27T15:40:00.000Z" new Date(2022, 10, 27, 15, 40, 12) // "2022-11-27T15:40:12.000Z" new Date(2022, 10, 27, 15, 40, 12, 333) // "2022-11-27T15:40:12.333Z"
The fact we always have to be mindful of month start from the index of 0 is a massive flaw. Many times, I needed to fix bugs around this when I forgot to subtract 1 when creating a new date. Here's a full explanation on StackOverflow if anyone is interested in knowing why this is the case.
This method's second and most significant problem is a lack of an easy way to create a UTC date. Whilst there is a workaround, it is long-winded and makes it difficult to read. Not to mention how easy it is to forget to add the
Date.UTC() part when creating a new date.
new Date(Date.UTC(2022, 10, 27)) // "2022-11-27T00:00:00.000Z" new Date(Date.UTC(2022, 10, 27, 15)) // "2022-11-27T15:00:00.000Z" new Date(Date.UTC(2022, 10, 27, 15, 40)) // "2022-11-27T15:40:00.000Z" new Date(Date.UTC(2022, 10, 27, 15, 40, 12)) // "2022-11-27T15:40:12.000Z" new Date(Date.UTC(2022, 10, 27, 15, 40, 12, 333)) // "2022-11-27T15:40:12.333Z"
Avoid Epoch Timestamps
Passing dates as Epoch Timestamps was common when I worked with PHP around 2015. However, I haven't seen this method used at all since I moved over to using Node in the backend.
First, one problem with this method is relying on the Timestamp to be in the correct time standard (e.g. UTC) to begin with. If we pass in a UTC timestamp, then we'll get a UTC date object, and vice-versa with non-UTC timestamps.
console.log(new Date(2000, 3, 0).getTime()) // 954457200000 console.log(new Date(Date.UTC(2000, 3, 0)).getTime()) // 954460800000
Secondly, timestamps are extremely difficult to comprehend for humans. There could be a bug in front of my eyes, and I wouldn't be able to spot it. So unless there is a solid reason to use this method of storing date, I would avoid it.
Avoid non-ISO8601-compliant string formats
Not sure I need to go into much detail regarding why
new Date("December 17, 1995 03:24:00") is a bad idea. The lack of a strict standard makes creating new date inefficient at best and error-prone at worst.
UTC, GMT, ISO and local time
Time zones are confusing to work with and we are barely scratching the surfaces here with these acronyms.
What is the difference between UTC and GMT?
Coordinated Universal Time (UTC) is a non-location-based time standard. It means no country on Earth uses it as a local time. Instead, think of it as international time. Greenwich Mean Time (GMT) is a time zone in some European and African countries. In the UK, we change between GMT and British Summer Time (BST) depending on Daylight Saving Time (DST).
It is always better to use UTC when possible. For example, it is a headache to consider time zones and daylight savings when we want to add two hours to a JS Date.
What is ISO Date?
International Organization for Standardisation (ISO) is an organisation that develops and publishes international standards. Essentially, they developed the concept of UTC.
ISO is only referenced by JS Date as an output format, e.g.
new Date().toISOString(), which returns a string like this
"2022-11-27T15:40:12.333Z". Since the value is always zero UTC offset, you could also think of it as
.toUTCString() (this is something I made up!).
By default, JS Date will use the local device to figure out the timezone and use it as a base for date manipulations. This can be desirable since it makes displaying Date on the client side much more straightforward as now, as developers, we don't need to consider time zone.
However, this approach has a massive problem if we ever need to work across different time zones. We also work across time zones more than you think. For example, clients and servers are often in different time zones. Sending a non-UTC date from a client to a server will usually result in bugs because the two sides have a very different understanding of the date's value.
Always use UTC
After running into several bugs, I finally decided to refactor my date library to always use UTC. One of the bugs I encountered was unit tests failing in the pipeline when they passed locally. This is because the pipeline's server was at a different timezone than my local machine.
Another reason why I'm so fixated on using UTC is that when we create a new date with non-UTC the output of
getUTCDate() is different in the example below.
// code to get the number of days in the current month new Date(2022, 3 + 1, 0) // 2022-04-29T23:00:00.000Z new Date(2022, 3 + 1, 0).getDate() // 30 new Date(2022, 3 + 1, 0).getUTCDate() // 29 <= wrong new Date(Date.UTC(2022, 3 + 1, 0)) // 2022-04-30T00:00:00.000Z new Date(Date.UTC(2022, 3 + 1, 0)).getDate() // 30 new Date(Date.UTC(2022, 3 + 1, 0)).getUTCDate() // 30 <= correct
It's also worth mentioning that using UTC doesn't mean we lose the ability to display the correct Date and time within our applications. On the client side, we can use
toLocaleDateString() to convert the UTC Date object back to any time zone we would like.