How Deep a Simple Problem Can Get: Moment, Node, Heroku & Time Zones

Over the weekend I started building my first real node.js application. I had watched the Hello Node series from TekPub, read the LeanPub books and attended NodePDX this year. I was ready to get down in the weeds and start writing a real application.

I also have been wanting to try to connect into the local non-.NET community in Olympia, not that I ever see my day job not involving .NET, but I am interested in learning different ecosystems, languages and frameworks I think it makes me a more well rounded better developer in the long run. So I started a meetup group for Olympia, WA Node users and beginners.

My idea for a node app, was to create a site that consumes the meetup api and displays upcoming meetings. Fairly, simple. You can see the result of my weekend worth of work here. The site is a simple twitter bootstrap based single page that has a carousel widget displaying the upcoming meetings, currently only one scheduled.

You can see meetup specific api data including the number of members who have said they were attending, the location and a google map link, as well as a date and time. I was pretty happy with myself and blasted the link out to the world via twitter and facebook. Little did I know I had missed something in the details, which Chris Bilson was so kind to point out. The date being displayed on the site said the meeting was being held at 1:30 AM.

The meetup api returns an event object containing two bits of information related to the event's date and time, time and utc_offset. The time is based on milliseconds since the UNIX Epoch. And the utc_offset is milliseconds based as well. Because I was in full on cowboy mode coding up a storm, my initial implementation of prettifying the date looked like this with no tests.

var moment = require('moment');

exports.helpers = {
	prettyDate: function(input) {
		return moment(input).format("dddd, MMMM Do YYYY h:mm:ss A");
	}
}

This node module uses the awesome Moment module to parse a UNIX Epoch number into a date and then format it using standard date formatting. This worked awesomely on my local machine. So I didn't think about it any more and moved on, until Chris chimed in.

Chris had suggested that it might have something to do with UTC. I was also a little embarrassed that I didn't have such a simple thing under unit test. So I started fixing the bug by getting the code under test. I had a couple well known values for the currently scheduled meeting.

var helpers = require('../lib/helpers').helpers;

describe('helpers', function(){

	describe('pretty date', function(){
		var input = { time: 1341279000000, utc_offset: -25200000 },
		    expected = 'Monday, July 2nd 2012 6:30:00 PM',
		    actual = helpers.prettyDate(input.time, input.utc_offset);

		it('prints a pretty date in the correct time zone', function(){
			expected.should.equal(actual);
		});
	});
});

The interesting thing here is the test passed with out modifying the implementation code at all. You see Moment automatically sets an offset based on the current environment. So if I were able to run this test on Heroku the test would fail. I was a bit stumped and came back around to my sad little cowboy ways and modified the implementation like this.

var moment = require('moment');

exports.helpers = {
	prettyDate: function(input_date, utc_offset) {
		return moment(input_date).utc()
		       .add('milliseconds', utc_offset)
		       .format("dddd, MMMM Do YYYY h:mm:ss A");
	}
}

I was grasping at straws, but this modification didn't effect the test running locally. I was curious what would happen when running the site on Heroku. I suspected I would have the same issue. I was very surprised to see that the code worked.

The downside was that I didn't understand why and that bugs the crap out of me. I couldn't let it go. Getting the code to work was not enough for me, I needed to understand why. So I started googling. I lucked out and found this blog post on Adevia Software's blog.

It clicked for me after that. The reason the test for the new code passed locally and the code worked on Heroku all had to do with the time zone settings of the environment running the code. My local environment is set to PST, so taking a Unix Epoch based date parsing it with Moment gives a PST date, which is then converted to UTC and then reduced by a PST UTC Offset resulting in the original PST date created by Moment.

Heroku's default apparently is UTC. Apply the same logic and you end up with a UTC date that has been reduced by 8 hours that is still a UTC date. It looks right on Heroku because my pretty printer doesn't include the timezone. If it had it would be wrong.

Once again I understood how the code worked and it was working, but it was wrong. The nag in the back of my head would not let it go. It's a bug, bugs must die. Now that I understood what was going on, I went back and reverted by helper back to this implementation.

var moment = require('moment');

exports.helpers = {
	prettyDate: function(input_date) {
		return moment(input_date).format("dddd, MMMM Do YYYY h:mm:ss A");
	}
}

I then issued the following command to Heroku from the commandline.

Derp:website cheezburger$ heroku config:add TZ=America/Los_Angeles

Finally redeployed the site and all is right with the world, I can get some work done now. Thanks, Bilson. This episode of OCD is brought to you by the letters W, T & F.

Follow me on Mastodon!