#2159·hermes

Date: a DST transition at local midnight makes setHours read back the wrong day

Author: CSealCreated Aug 27, 2026Updated Sep 14, 2026

Summary

When a timezone's DST transition happens at local midnight, Date converts a wall-clock time to an instant using the UTC offset in force before the jump, then reads that instant back using the offset after it. A time in the last hour of the preceding day therefore reads back as the next day.

Repro

javascript
process.env.TZ = 'Europe/Kiev' // or run the device in that timezone

const d = new Date(1984, 2, 31)   // 31 March 1984, local
d.setHours(23, 59, 59, 999)
console.log(d.getDate())
result
Hermes 1
V8 / JSC / Node 31

Kyiv moved its clocks at 00:00 from 1981 through 1984, so 1984-04-01T00:00 local does not exist. Hermes resolves 1984-03-31T23:59:59.999 to an instant that it then formats as 1 April.

Instrumented on device:

start     1984-03-30T22:00:00.000Z   getDate=31
setHours  1984-03-31T21:59:59.999Z   getDate=1

The forward conversion applied no DST term; reading the same instant back applied one.

Why this matters beyond a corner case

dayjs implements daysInMonth() as endOf('month').date() - take the last day of the month, setHours(23, 59, 59, 999), read the day. Under this bug that returns 1 instead of the real length, and every consumer that builds a month grid from it renders a single cell.

We hit it through react-native-ui-datepicker, whose getDaysInMonth feeds dayjs().daysInMonth() straight into Array.from({ length }). A user picking a birth date in March 1984 saw a calendar with one day in it.

Scanning every month from 1930 to 2030 on the device under Europe/Kiev gives exactly four wrong answers: March 1981, 1982, 1983, 1984 - the years the transition was at midnight. From 1985 Kyiv moved it to 02:00 and the results are correct again.

This is not limited to historical dates. Any zone that still transitions at local midnight is exposed for its current month: Chile, Cuba, Iran, Paraguay and parts of the Levant among them.

Possibly related

On the same device, Hermes reports Kyiv as UTC+2/+3 for 1984, while Android's own tz database (TZ=Europe/Kiev date in the shell) reports MSK +3 / MSD +4, matching macOS and Node. Hermes appears to carry the zone's current standard offset backward through history. That does not cause the bug above, but it may share a root.

Environment

  • Hermes v0.17.0, as shipped with React Native 0.86.2
  • Android 16, Samsung SM-S916B (arm64-v8a)
  • Device timezone Europe/Kiev

Workaround

Compute month length with Date.UTC, which has no offset to resolve:

javascript
const daysInMonth = (year, month) => new Date(Date.UTC(year, month + 1, 0)).getUTCDate()