heroPhoto by Towfiqu barbhuiya on Unsplash

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

Like most things in JavaScript, JS Date is very flexible when parsing different date formats. Unfortunately, flexibility often comes with its issues and drawbacks. Let's first take a look at what is possible.

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 Z suffix, e.g. new Date("1992-12-17T12:50Z").

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!).

Local time

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 getDate() and 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.

Buy Me A Coffee