Fixing JavaScript Date – Web Compatibility and Reality

In my previous post, I discussed things that could be fixed in JavaScript’s date implementation – if we wanted to. In this post, I’ll discuss things that can’t be fixed – no matter how much we want them to be.

No Semantic Versioning Here!

Most developers are familiar with Semantic versioning, or semver. In semver, we have the idea of three kinds of release:

  • Patch – releases that fix bugs only
  • Minor – releases that add small features
  • Major – releases that break things, so you have to change code

Semver is amazing. It clearly describes what versions we can safely upgrade to, when we should look for new features, and when we should expect to have to change code that is broken. It enables awesomeness. Unfortunately, it is not a reality for JavaScript.

A blessing and curse of JavaScript is that it is currently the world’s most popular programming language. Because of the sheer number of users, it’s safe to assume that anything that can be expressed with the language, has been expressed with the language, even if the code is what any sane person would consider terrible.

In addition, terrible code from 1998 is still being served to browsers everywhere, and there aren’t enough devs in the world to go update all of that code.

This results in two very important concepts that members of TC39 must constantly keep in mind:

  1. Web Compatibility – No change made to ECMAScript can be incompatible with the existing behavior of ECMAScript
  2. Web Reality – If code currently behaves a certain way, future versions of the spec should continue to have it behave that way – even if the behavior present is not described in the spec.

These concepts can really be summed up with the words “Don’t break the web!”. This creates a reality where there can be no such thing as a semver ‘major release’ in JavaScript. This idea drives every moment of the TC39 process, and has resulted in some unpopular but necessary compromises in the spec.

Mutability – a Web Compatibility Problem

I am a big fan of Domain Driven Design by Erik Evans. In the DDD world, objects can be differentiated as Entities which change over time and are tracked by their ID, and Value Types which are defined by their properties.  Under this definition, a DateTime is a value type. If any property of the date changes (for instance, the month changes from January to Feburary), the date is certainly a different date. Currently though, JavaScript doesn’t really work this way. Consider the following:

var a = new Date();
a.toISOString(); //"2017-04-05T05:57:53.350Z";
a.setMonth(11);
a.toISOString();//"2017-12-05T05:57:53.350Z";

As you can see, the value of object a changes. Yet, April and December are certainly different months, and these are certainly different dates. This kind of behavior sets people up for nasty bugs down the road. For instance, the following code will not behave as expected:

function addOneWeek(myDate) {
    myDate.setDate(myDate.getDate() + 7);
    return myDate;
}

var today = new Date();
var oneWeekFromNow = addOneWeek(today);

console.log(`today is ${today.toLocaleString()}, and one week from today will be ${oneWeekFromNow.toLocaleString()}`);
//today is 4/16/2017, 10:58:10 AM, and one week from today will be 4/16/2017, 10:58:10 AM

 

WOAH! This is no good. A better, and less bug prone behavior, would be to have the setters on the Date object return a new instance of the date – or for dates to be immutable. Then, the above code could be refactored to this common sense code:

function addOneWeek(myDate) {
    return myDate.setDate(myDate.getDate() + 7);
}
var today = new Date();
var oneWeekFromNow = addOneWeek(today);

console.log('today is ${today.toLocaleString()}, and one week from today will be ${oneWeekFromNow.toLocaleString()}');
//today is 4/09/2017, 10:58:10 AM, and one week from today will be 4/16/2017, 10:58:10 AM

Unfortunately, this is not to be because of a Web Compatibility issue. In short, if we were to make this change, tons and tons of code that relies on date being mutable (including the entire Moment.js library, BTW) would be broken.

Broken Parser – A Web Reality Issue

The ECMA262 standard currently describes very few rules for parsing date strings. These few excerpts are of particular interest:

ECMAScript defines a string interchange format for date-times based upon a simplification of the ISO 8601 Extended Format. The format is as follows: YYYY-MM-DDTHH:mm:ss.sssZ

This quote makes perfect sense. It states that JavaScript uses ISO8601 format as it’s main date interchange format. Since this is the most common interchange format for dates in modern computing, this is a great start! The standard then describes ISO8601 format options briefly:

This format includes date-only forms:

YYYY
YYYY-MM
YYYY-MM-DD

It also includes “date-time” forms that consist of one of the above date-only forms immediately followed by one of the following time forms with an optional time zone offset appended:

THH:mm
THH:mm:ss
THH:mm:ss.sss

So basically, you can have date only ISO8601 formats, and combined date-time formats. All good. But then a few lines further down, you get this wonderful quote:

When the time zone offset is absent, date-only forms are interpreted as a UTC time and date-time forms are interpreted as a local time.

In practice, what does this mean? Consider the following code:

new Date('2017-04-08').toISOString()
//"2017-04-08T00:00:00.000Z"
new Date('2017-04-08T08:30').toISOString() 
//"2017-04-08T15:30:00.000Z"

Basically, when I didn’t specify a time, the value was interpreted as a UTC value, but when I did specify a time it was interpreted as local. This is all… a bit mad. The next question one would ask is “is this some oddity of the ISO8601 specification?” But in fact that spec dictates that absent an offset, the time should be interpreted as local – meaning that under ISO8601 both values above should have been read as local time.

What happened here was a weird ‘Web Reality’.

In the days of ES5, the specification read this way:

The value of an absent time zone offset is “Z”.

This is saying that absent an offset, the time zone should be interpreted as UTC – the exact opposite of what the ISO8601 spec says. TC39 realized their error, and in ES2015 they corrected to this:

If the time zone offset is absent, the date-time is interpreted as a local time.

This is how the spec should read, as it aligns with ISO8601’s standard, and how basically every other date time API works. So why was it changed? Because as browsers started shipping this change, they started getting tons of bug reports that times weren’t being interpreted as they had been before – which of course they weren’t. By the time TC39 was able to give this issue more attention, the ecosystem was stratified, with some browsers picking UTC, and others picking Local, and others still making unique code compromises. After much evaluation of user feedback about what was expected to happen, the committee settled on the text as it is today, because it was the ‘reality’ of how the web worked – even if it wasn’t correct by the definition of the standard, or even particularly logical.

Given the amount of pain changes to this part of the spec caused the greater community, it is the case that it can’t be changed. The ‘web reality’ of what people expect from Date’s current parser will not allow it.

What do Do?

These two things were the impetuous for Matt, Brian, and I to choose to introduce a new datetime handling object to JavaScript, giving us a clean slate to make the world right. Currently we call this object ‘temporal’. This proposal can be found here, but look for a future post discussing our choices.

Leave a comment