JavaScript Dates Off By One

March 13, 2021    

I was recently watching a live stream by Matthew Clemente where he was trying to wrap his head around a datetime issue he was seeing with this Jekyll -> Eleventy conversion project. He actually had two problems: one was that his dates were off by one day and the other was that Eleventy implicitly converts a short date string to a date object automatically. We're not going to cover the latter issue today.

Here's a link to the live stream where he experienced what is about to be explained.

The Setup

If you're not familiar with how many static-site generators work, they use something called Front Matter to define metadata about a page. In this case, a blog post with a publish date. Here's an overly simplified example:

title: The Time Traveler's Conundrum
date: 2021-03-12

# Page markup goes here

This YAML code goes at the top of pages, whether they are blog posts or regular content pages. The site generator will evaluate this markup and use it to help build the page. The variables declared in Front Matter can be expressed on the page using template tags for whichever template library is being used.

The Problem

If you were to take a string, give it to the date object, and then log the result in the console, you might experience something like this:

const d = new Date( "2021-03-12" )
console.log( d.toLocaleDateString( "en-US" ) )
// 3/11/2021

Hold on. That says it's the 11th but I asked for the 12th. So what gives?

When the date object is created and you don't give it a time, the browser will assume that you want midnight UTC. Essentially, you've done this:

const d = new Date( "2021-03-12T00:00:00Z" )
console.log( d.toLocaleDateString( "en-US" ) )
// 3/11/2021

Notice that the "Z" on the end means you're not providing a time zone, or rather that you're asking for zero UTC offset.

If you've got a negative time zone offset, your formatted time is going to be a day sooner than expected. JavaScript is storing the date internally as midnight UTC but converting it to the browser's local time when rendering. In other words, if my UTC offset is -5, then 00:00 on March 12, 2021 in UTC is actually 17:00 on March 11, 2021 in my time zone.

Possible Solutions

Solution 1: Provide a time part to your date

If a time part (without time zone) is provided to new Date() then the browser will assume we want the time in the local time zone.

const d = new Date( "2021-03-12T00:00:00" )
console.log( d.toLocaleDateString( "en-US" ) )
// 3/12/2021

Solution 2: Provide a time zone in the DateTimeFormat options

One of the DateTimeFormat options for the Intl JavaScript library is timeZone. If you don't really need time zone support – and in this case we don't – it may be permissible for you to ignore time zones altogether and work with the date in this fashion.

const d = new Date( "2021-03-12" )
console.log( d.toLocaleDateString( "en-US", { timeZone: "UTC" } ) )
// 3/12/2021

Solution 3: Use a date formatting library

You could also evaluate the usage of a library such as Luxon for more robust formatting options. Admittedly, the built-in JavaScript formatting options often leave something to be desired, so using a library dedicated to formatting dates may be prefered.