It's time we stop using JS Date
Every dev team on the planet has at one point run into issues with their test suite or applications from improperly handled dates.
Sure, if always properly handled you can avoid these issues, but it’s so easy for issues to sneak in.
This took me a while to come around to, but now that I’ve converted a large codebase over, I’m never looking back.
To me JS Dates are dead forever.
Motivations
While building a work platform, I’ve had to store multiple types of dates:
Dates that apply to everyone (e.g. “2021-06-02” no matter your timezone)
DateTimes that apply to everyone (e.g. Every monday at 5pm close your windows)
DateTimes that indicate a specific timezone (e.g. Get it to me by tomorrow 5pm Sydney time)
Dates that originally apply to everyone (e.g. this template repeats every Tuesday), but then once created, are in a specific timezone.
Trying to debug with JS Dates is a nightmare because it’s easy to lose track of which timezone your client, server, and database are showing you the date in. (Yes even when force everything in UTC).
It’s also then impossible to have a date in your hand and know what which one of the above it represents.
Guiding Principles
Prefer Functional patterns over Object-Oriented
Separation of data and behavior
No side-effects
Immutable Data
Predictable results
Easy to debug
(If you don’t like these, this probably isn’t for you.)
Why JS Dates are bad
We think of them as Primitives, but they are a big dirty class with lots of baggage
combine data with behavior (bad practice in FP)
mutable (bad practice in FP)
rely on side-effects (system timezone) which means functions return different results depending WHERE they are run
subtle changes in behaviour when constructing from strings that many are not aware of
not supported by JSON parsing (really) and so has asymmetrical serialization/deserialization
very OO and stateful
non-obvious behaviours – some functions return local time, some UTC (docs constantly have to specify which function uses which).
not easy to debug (showing values based on timezone, etc)
Examples
1. You might not have know this gotcha
When a timezone (Z or +00:00)
in excluded, dates are interpreted as local time, not UTC. Easy to miss when debugging.
2. Hard to write pure functions (the following have side-effects):
Depending on where you run this code you will get a different result, because constructing a date without the Z
forces the date to be in the local timezone (not obvious).
3. Not supported by JSON
Despite everyone acting like it, Dates are not a supported in JSON or GraphQL.
This gets particularly fiddly when combined with the above issue.
Since serialization/de-serialization is often done by libraries in the middle, it’s not always obvious what’s going on.
Leads to side-effects that and hard-to-debug issues.
4. You can’t specify partial dates:
If you wanted to represent the data 2021-06-02
with a Date, it will always be interpreted as 2021-06-02T00:00:00Z
which is not the same thing. undefined !== 0
.
This leads to the hack that everyone uses to represent a day in JS: using T00:00:00
. But this is a hack and not a proper representation of the data and can lead to issues such as point 1.
5. Unable to differentiate between timezone-less dates and UTC dates
Likewise, if you want to store the date 2021-06-02
in ANY timezone, you can’t. It will always be instantiated with a timezone of Z
(and a time of 00:00).
However they are different things. 2021-06-02
is the idea of a day for anyone anywhere. 2021-06-02T00:00:00Z
is a hack we use to represent that.
6. One-way conversion (data-loss)
When you have a Date in your hand, you don’t know what it is representing.
This is because going from a ISO-8601 RFC-3339 format string such as 2021-06-02T00:00:00+4000
converts to a JS Date in your local timezone, and there is no way to go back to your original input.
The Solution: ISODate
Note: I really didn’t like this at first, and it took a while for me to come around to the idea.
So the solution is weirdly…. to always store dates as strings…
But with a few rules:
Only support the ISO-8601 RFC-3339 format.
Only support UTC storage (Z), never store timezone offsets in the string (+0200).
Use typescript’s new Template Literal Types to enforce the format.
A simple definition like this gives you the gist. We’ll build on this to support all cases however.
First, naming conventions for talking about dates
Since undefined !== 0
, we need a way to talk about dates that differentiates when a date is qualified to a timezone versus not.
I am proposing the below:
1. Calendar Dates
Think of a physical, pin-on-the-wall calendar. The Platonic idea of 12 months, 30-ish days in a month, 24 hours in a day, etc.
The Calendar Date of “2nd of June 5pm” is just that. It represents the second day of the month of June at 5pm.
The “2nd of June 5pm” can “occur” many times (e.g., in different timezones, planets, etc).
You can convert calendar dates to timezone dates by specifying a timezone.
2. Timezone Dates
Think of a linear timeline from the Big Bang to the present moment.
When you want to a single point in time on that line, you are referencing a definitive moment in time.
Timezone dates occur ONCE everywhere, all at once (General Relativity ignored).
So the Timezone date of “2nd of June 5pm” needs to be qualified more with a location: the point in time when it was 5pm for me in Kansas.
Timezone dates, therefore, are qualified with another piece of information—that the time is denoted in UTC (Z).
General note: We don’t store time offsets in dates because time offset !== timezone
thanks to daylight savings, etc.
Supporting timezone and calendar dates
Ok but how do you use them?
All date libraries, formatting, etc needs a JS Date Object.
The key to avoid any issues with timezones is:
Convert & use the date in an atomic operation.
Always specify the context you want to manipulate the date in (UTC or Local).
What this looks like in practice is a useDate
function that takes an ISODate and does some sort of operation, transformation, or formatting with it.
The DateMode
is important because it tells the function how to interpret the date, and essentially the “context” you want to manipulate the dirty JSDate in.
What this looks like in practice:
This is where you can still get into trouble if you have a CalDate (e.g. "2021-06-02") and try to convert it to a JSDate without specifying the mode, does that calendar date mean "2021-06-02T00:00:00Z" or "2021-06-02T00:00:00" local time?
Benefits
Why ISO-Date approach is better:
clear and predictable – always know what the date is representing
easy to debug – no side-effects, no timezone issues
easy to convert to and from JS Date
no issues converting to-and-from JSON
any side-effects are contained in the function that does the work with it
can be used to do transformations, formatting, etc, but always returns to ISODate format
Questions / Complaints
“The
use*
naming convention is normally used for Hooks, is this a hook?” – No this is not a hook, but theuseDate
naming seemed fitting here.“Why do you need DateMode?” – because JSDate will always force a date into a timezone, to keep the function pure, the mode will then tell the function how to interpret the date. E.g. mode ‘local’ means interpret the date
2021-02-02
as2021-02-02T00:00:00
in the local timezone.“Storing it as a
string
means you can put anything in there.” – Yes, but did you know thatnew Date("blah")
does not throw an error. You have to probe it with.isValid()
to know if it’s a valid date. So really it’s no different. Check if a string is a valid ISODate withisISODate
.“This is a lot of work for something that’s not broken.” – sure
Complete ISO-Date Proposal
Also available as a gist here.
Follow the journey
I’ve been in stealth mode for a year now building a team operations platform that puts the current “project management” and “team docs” products to shame (not naming names).
Early access will be given out to people following here on substack.