setDate(latest.getDate() - 1) skips 2021-11-07, but only when run for some people

I have a bizarre problem here. In an app I'm making, a need to generate an array of consecutive date strings, where each date string is 'YYYY-MM-DD'. It worked fine for a long time, but then in November one of my users let me know that one date, 7 November 2021, was missing from the app.

But, it wasn't missing for anyone else. So I worked with him to narrow down the problem to the code below. It behaves differently if I or him run it in our Chrome consoles.

// Creates an array of 14 consecutive date strings
function datesInclGaps(date, hour) {
    const latest = toUTCDate(date, hour)
    const result = []
    for (let i = 0; i < 14; i++) {
        result.push(fromUTCDate(latest))
        latest.setDate(latest.getDate() - 1)
    }
    return result
}

// Creates a Date object with the given UTC date and the give hour.
const toUTCDate = (date, hour) => {
    const match = /(\d\d\d\d)-(\d\d)-(\d\d)/.exec(date)
    if (!match) throw new Error('Illegal date format: ' + date)
    const utc = new Date()
    utc.setUTCFullYear(+match[1], +match[2] - 1, +match[3])
    utc.setUTCHours(hour, 0, 0, 0)
    return utc
}

// Returns a date string YYYY-MM-DD from a Date obj.
// Date assumed to be in UTC.
const fromUTCDate = date => {
    const y = date.getUTCFullYear()
    const m = pad(date.getUTCMonth() + 1)
    const d = pad(date.getUTCDate())
    return `${y}-${m}-${d}`
}

const pad = (t, p = 2) => t.toString().padStart(p, '0')

console.log('0-hour')
console.log(datesInclGaps('2021-11-14', 0).join(', '))
console.log('12-hour')
console.log(datesInclGaps('2021-11-14', 12).join(', '))

The list of dates should both be the first 14 days of November. The 'hour' argument is how I finally 'fixed' the bug, since if I set it to 0, the day disappears for him, but if it is set to 12, it works fine. See the screenshots below. The light-mode is his output, with the error, and the dark mode is when I run it, with no error. Exact same simple plain Javascript run in the chrome console.

Example of run where it works fine, both 0 and 12 hour

Example of run where 0 hour fails, but 12 hour works (Oklahoma City)

The reason I use the UTC versions is to make sure that timezones wouldn't matter. But... it is the only thing I can think of that might be a difference between us. I've racked my brain over this one but I cannot think of a single good reason why the code is behaving like this! Any insight would be useful!


This happens because you use getDate: this is a method that works with local date. If the time zone difference is such that a UTC midnight time is still in the previous day, then getDate will return that previous day's date. But on the first day that daylight saving changes, moving local time one hour backward, subtracting 1 day in local timezone terms, corresponds to subtracting 25 hours in UTC terms!

So we have at a certain moment this Date:

 8 November 2021, 0:00 UTC

When doing getDate(), the date is interpreted as a local date, in timezone GMT+00 without Day Light Saving. So we get:

 8 November 2021, 0:00 Local Time Zone

Then we do setDate with 1 day subtracted:

 7 November 2021, 0:00 Local Time Zone

But this date happens to be in the period of Day Light Saving, and so it translates back to this UTC Date:

 6 November 2021, 23:00 UTC

So when this date is output, it is 6 November, not 7 November, which got skipped.

The solution is quite obvious when you see it: you should use getUTCDate and setUTCDate and stay away from the non-UTC variants of these methods, since you initialised the date with UTC time.