facebook / facebook/hermes

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

Open
#2,159 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
11.3k
Forks
859
Avg merge
1h 30m
Merged PRs (30d)
3

Description

## 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

```js
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`](https://github.com/farhoudshapouran/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:

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

Contributor guide

Open the contributing guide

Research direction

Reproduce the Europe/Kiev case using JavaScript Date, especially setHours() followed by getDate(). Trace Hermes’s Date timezone conversion around the local-midnight DST transition and compare the result with the documented V8, JSC, and Node behavior. Done means the repro returns 31 and month-length calculations no longer produce incorrect results for the affected dates.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.