Repository navigation
Date uses the old timezone after the device timezone changes — React Native never calls IHermes::resetTimezoneCache() #59131
Description
Description
When the device timezone changes while the app process is alive, every local Date
computation in JavaScript keeps using the previous zone until the app is killed and cold
started. new Date().getTimezoneOffset(), getDate() / getHours() / getMonth(),
new Date(y, m, d) and toString() all report the old offset. At the same time,
Intl.DateTimeFormat().resolvedOptions().timeZone reports the new zone — so two standard
APIs disagree about the current zone inside the same process.
Cause. Hermes caches the local UTC offset and does not detect system timezone changes. It
exposes IHermes::resetTimezoneCache() for exactly this, and its own header documents the
contract (ReactCommon/jsi/jsi/hermes-interfaces.h):
Hermes maintains a cached timezone offset to accelerate date and time calculations. However,
this cache does not automatically detect changes to the system timezone. When the system
timezone changes, the integration layer (e.g., React Native) must call this method.
React Native never calls it. We checked 0.85.1, 0.87.0, 0.87.1 and 0.88.0: there is no caller outside that header,
and no NSSystemTimeZoneDidChangeNotification / ACTION_TIMEZONE_CHANGED observer anywhere.
Hermes closed facebook/hermes#1751 on that basis ("The integrator should call
IHermes::resetTimezoneCache()"). The only attempt in React Native, #55467, is Android-only
and has been stalled since February on a New-Architecture review comment.
What goes wrong: a correct-looking date and time with a stale timezone
After the zone change, Date still pairs every local date and time with the origin's UTC
offset. The result is a datetime that looks right but is not valid for the device's real zone —
and once persisted, nothing downstream can tell. It shows up in two ways:
-
Creating a datetime from local values — a date/time picker, "today at 09:00",
new Date(y, m, d, h, min), parsing a local timestamp without an offset. The date and time
digits are exactly what the user meant, but they are combined with the stale offset:user means
2026-01-15 09:00 +13:00(Auckland) → app holds2026-01-15 09:00 −10:00(Hawaii)On screen it looks correct — and it cannot look anything else, because user interfaces show
the date and the time but almost never the timezone. The one part that is wrong is the one part
nobody sees: the user checks "Thu 15 Jan, 09:00", confirms it, and saves. Persisted —
toISOString(), epoch milliseconds, a server timestamp — it is a different moment, off by the full gap between the zones: hours in the
future after flying one way, hours in the past after flying the other. Every later
calculation, sync and comparison inherits it. This is the worst case: the value looks right
and is wrong. -
Reading local values from "now" —
new Date()itself holds the correct moment (an
instant does not depend on a zone), but every local view of it uses the stale offset:
getHours(),getDate(),toString(), "start of today". It is 09:00 in Auckland and
toString()reportsWed Jan 14 2026 10:00 GMT−1000— a consistent description of the
moment, in the wrong zone. Any local date or time derived from it and saved (a record date,
"today") is just as wrong.
Pure instants (Date.now(), getTime(), toISOString() of an instant taken from the clock,
getUTC*) and a freshly built Intl.DateTimeFormat are unaffected — which is exactly why
Date and Intl disagree about the current zone inside one process.
It happens whenever the process survives a zone change: a phone changes zone by being carried — a
flight, a drive or a train across a border — usually with the app backgrounded, not killed, and
iOS and Android routinely keep such a process alive for hours or days. Automatic zone switching on
arrival, or a user changing the zone by hand, has the same effect. Nothing self-corrects until a
cold start, and a value that was saved stays wrong after it.
Examples — any change of timezone is enough
This is not about long-haul flights or the date line. The moment the device enters a zone with a
different UTC offset, every datetime the app builds carries the old one. The extreme case
below only makes the size of the error obvious; a one-hour hop is just as wrong.
The app is started at the origin, kept alive, and used after arrival. The user enters
"Thu 15 Jan 2026, 09:00" (January offsets; the digits are identical in every row — only the
offset differs):
| Trip | User means | App holds (stale offset) | …which is really, locally | Error |
|---|---|---|---|---|
| Next-door hops | ||||
| London → Dublin | 09:00 +00:00 | 09:00 +00:00 | Thu 15 Jan, 09:00 | none — same offset |
| Dublin → Copenhagen | 09:00 +01:00 | 09:00 +00:00 | Thu 15 Jan, 10:00 | 1 h in the future |
| Copenhagen → London | 09:00 +00:00 | 09:00 +01:00 | Thu 15 Jan, 08:00 | 1 h in the past |
| Copenhagen → Helsinki | 09:00 +02:00 | 09:00 +01:00 | Thu 15 Jan, 10:00 | 1 h in the future |
| Within the US | ||||
| New York → Chicago | 09:00 −06:00 | 09:00 −05:00 | Thu 15 Jan, 08:00 | 1 h in the past |
| New York → Denver | 09:00 −07:00 | 09:00 −05:00 | Thu 15 Jan, 07:00 | 2 h in the past |
| New York → Los Angeles | 09:00 −08:00 | 09:00 −05:00 | Thu 15 Jan, 06:00 | 3 h in the past |
| Los Angeles → New York | 09:00 −05:00 | 09:00 −08:00 | Thu 15 Jan, 12:00 | 3 h in the future |
| Long-haul | ||||
| London → New York | 09:00 −05:00 | 09:00 +00:00 | Thu 15 Jan, 04:00 | 5 h in the past |
| New York → London | 09:00 +00:00 | 09:00 −05:00 | Thu 15 Jan, 14:00 | 5 h in the future |
| Copenhagen → Dubai | 09:00 +04:00 | 09:00 +01:00 | Thu 15 Jan, 12:00 | 3 h in the future |
| London → Singapore | 09:00 +08:00 | 09:00 +00:00 | Thu 15 Jan, 17:00 | 8 h in the future |
| Extreme (date line) | ||||
| Hawaii → Auckland | 09:00 +13:00 | 09:00 −10:00 | Fri 16 Jan, 08:00 | 23 h in the future |
| Auckland → Hawaii | 09:00 −10:00 | 09:00 +13:00 | Wed 14 Jan, 10:00 | 23 h in the past |
- The error is exactly the offset difference — 1 hour for a short hop within Europe or the
US, several hours long-haul, up to a day across the date line. Flying towards later zones the
moment lands in the future; towards earlier zones, in the past. - London → Dublin is the control: same offset, no error. The cause is a changed offset, not
distance — so it also covers crossing a land border, a train ride, or a user changing the zone
by hand. - On screen every row reads "Thu 15 Jan, 09:00". "User means" and "App holds" differ only in
the offset — the part a date or time field does not display. - "Now" is shifted the same way when read: after New York → Los Angeles, at 09:00 local time
new Date().toString()reports12:00 GMT−0500. - The size of the error also depends on the date — see daylight saving below.
Daylight saving makes the error unpredictable
The gap between two places is not fixed. It depends on whether each one observes daylight saving
and when: the US, Europe and the southern hemisphere switch on different dates, and many places
never switch (Arizona, Hawaii, Iceland, most of Asia). So the same trip produces a different error
depending on the day of the year — and a route can be harmless half the year and wrong the other
half. Gap between the two places on each date in 2026:
| Route | 15 Jan | 15 Mar | 31 Mar | 15 Apr | 15 Jul | 15 Oct | 28 Oct | 5 Nov | 15 Dec |
|---|---|---|---|---|---|---|---|---|---|
| Dublin ↔ Copenhagen | 1 h | 1 h | 1 h | 1 h | 1 h | 1 h | 1 h | 1 h | 1 h |
| London ↔ New York | 5 h | 4 h | 5 h | 5 h | 5 h | 5 h | 4 h | 5 h | 5 h |
| Copenhagen ↔ New York | 6 h | 5 h | 6 h | 6 h | 6 h | 6 h | 5 h | 6 h | 6 h |
| New York ↔ Phoenix | 2 h | 3 h | 3 h | 3 h | 3 h | 3 h | 3 h | 2 h | 2 h |
| Los Angeles ↔ Phoenix | 1 h | 0 h | 0 h | 0 h | 0 h | 0 h | 0 h | 1 h | 1 h |
| London ↔ Reykjavik | 0 h | 0 h | 1 h | 1 h | 1 h | 1 h | 0 h | 0 h | 0 h |
| London ↔ Singapore | 8 h | 8 h | 7 h | 7 h | 7 h | 7 h | 8 h | 8 h | 8 h |
| Copenhagen ↔ Sydney | 10 h | 10 h | 9 h | 8 h | 8 h | 9 h | 10 h | 10 h | 10 h |
| London ↔ Auckland | 13 h | 13 h | 12 h | 11 h | 11 h | 12 h | 13 h | 13 h | 13 h |
2026 switch dates: US 8 Mar → 1 Nov; EU and UK 29 Mar → 25 Oct; Australia (NSW) 4 Oct → 5 Apr;
New Zealand 27 Sep → 5 Apr; Arizona, Singapore and Iceland never.
- London ↔ Auckland has three different errors in one year — 13, 12 or 11 hours — because the
two hemispheres switch in opposite directions on different dates. - Los Angeles ↔ Phoenix is harmless all summer and an hour wrong all winter; London ↔ Reykjavik
is the reverse. A test run in the "wrong" season will not reproduce what users see in the other. - London ↔ New York drops to 4 hours for a few weeks in March and around the end of October,
when the US and Europe have switched on different dates.
The error cannot be predicted from the route alone: it depends on the origin, the destination
and the date. Any compensation outside the engine would have to replicate the world's daylight
saving rules — which is exactly what the engine already has, once its cache is reset.
What breaks, concretely
- Records saved hours off — an appointment, booking, timesheet entry or trip log created after
arrival is stored as a moment up to a day in the future or the past, and synced that way;
restarting the app does not fix the record. Where a local date is derived and saved (an expense
or receipt date), it lands on the wrong day — and at a month end, in the wrong reporting period. - Wrong clock times shown — timestamps, "created at" labels, durations, working-hours and
opening-hours checks are all off by the gap. - "Today" is wrong — today / yesterday / tomorrow labels, overdue and due-soon states,
day-grouped lists, streaks and daily limits all use the old day boundary. - Scheduling is shifted — reminders, bookings and calendar entries created after the change
land hours off; "remind me at 09:00" fires at the origin's 09:00. - Date pickers open on the wrong day — "default to today" uses the old date, and the user
accepts it without noticing. - No application-level fix reaches libraries — date pickers, calendars and date utilities all
readDate. Because the primitive is wrong, careful application code cannot compensate, and a
per-call-site workaround never covers third-party code.
Why it goes unnoticed
The wrong part is invisible by design. Date and time fields, pickers, lists and receipts
display a date and/or a time — almost never a timezone or UTC offset. A value with the right digits
and a stale offset is indistinguishable on screen from a correct one, so neither the user who
enters it nor anyone reviewing it later can catch it. The error only surfaces downstream — when
the stored moment is compared, synced, scheduled or converted — long after it was "confirmed".
Development and QA almost never change the device timezone while the app is running, and a cold
start always looks right. The symptom reaches users as "my appointment moved" or "my receipt has
the wrong date after my trip", which is rarely traced back to the JavaScript engine.
Related: facebook/hermes#1751 (Hermes hands the reset to the integrator), #55467 (Android-only
attempt). A PR covering both platforms on the New Architecture is in preparation and will
reference this issue.
Steps to reproduce
The mechanism reproduces deterministically at any time of day (the symptom above needs the
right time of day; this does not):
- Run the reproducer on a physical iOS device (the iOS Simulator follows the Mac's timezone)
or an Android device/emulator. - Note the three values on screen, e.g. offset
+120,IntlzoneEurope/Copenhagen. - Background the app (do not kill it). In Settings, change the timezone (e.g. to
Australia/Sydney; on Android this must be done by hand in Settings). - Return to the app. The screen refreshes on resume.
- Observed:
Intlzone =Australia/Sydney, butgetTimezoneOffset()still gives+120
andtoString()still showsGMT+0200; the screen's "agree?" row reads NO. - Kill and relaunch: all values agree on
+600.
Expected: after step 4, -new Date().getTimezoneOffset() is 600 and all values agree.
These steps are not an artificial trigger — they are a trip compressed into a minute. On
arrival, the phone's automatic time zone (from the cellular network or location) changes the
system zone: the same system setting the manual change in step 3 sets, delivering the same
notification to apps (Intent.ACTION_TIMEZONE_CHANGED on Android,
NSSystemTimeZoneDidChangeNotification on iOS). Hermes cannot tell the two apart and ignores both.
And the app being backgrounded across the change is the normal case on a real trip — the phone
sits in a pocket or in flight mode for hours while the process survives. A user who travels with
the app in the background gets exactly the values in the tables under Stacktrace or Logs.
React Native Version
0.88.0
Affected Platforms
Runtime - Android, Runtime - iOS
Output of npx @react-native-community/cli info
System:
OS: macOS 27.0.1
CPU: (10) arm64 Apple M1 Pro
Memory: 281.73 MB / 32.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 25.9.0
path: /opt/homebrew/bin/node
Yarn: Not Found
npm:
version: 11.12.1
path: /opt/homebrew/bin/npm
Watchman:
version: 2026.03.30.00
path: /opt/homebrew/bin/watchman
Managers:
CocoaPods:
version: 1.16.2
path: /opt/homebrew/bin/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 25.2
- iOS 26.2
- macOS 26.2
- tvOS 26.2
- visionOS 26.2
- watchOS 26.2
Android SDK: Not Found
IDEs:
Android Studio: Not Found
Xcode:
version: 26.3/17C529
path: /usr/bin/xcodebuild
Languages:
Java:
version: 21.0.6
path: /usr/bin/javac
Ruby:
version: 2.6.10
path: /usr/bin/ruby
npmPackages:
"@react-native-community/cli":
installed: 20.2.0
wanted: 20.2.0
react:
installed: 19.3.0
wanted: 19.3.0
react-native:
installed: 0.88.0
wanted: 0.88.0
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: true
iOS:
hermesEnabled: true
newArchEnabled: true
(Node 25.9 is outside the declared engines range; it only affects the local install
(`--ignore-engines`) — the bug is in the app runtime on the device.)
Stacktrace or Logs
No crash — wrong values. Measured on 2026-08-12 inside the live Hermes runtime (via the
inspector), same process ID across a Copenhagen → Sydney change, iPhone 17e (iOS) and Galaxy S24
(Android 16), RN 0.85.1, Hermes V1 — **identical on both platforms**:
| Probe | Before (CPH, +120) | After change (SYD, true +600) |
|---|---|---|
| `-new Date().getTimezoneOffset()` | 120 | **120** — stale |
| `Intl.DateTimeFormat().resolvedOptions().timeZone` | Europe/Copenhagen | **Australia/Sydney** — live |
| `new Date().toString()` | GMT+0200 | **GMT+0200** — stale |
**Reproduced again with the reproducer on React Native 0.87.1** (Galaxy S24, Android 16, Release
build, 15 October 2026), one process throughout — the zone was changed by hand in Settings,
Copenhagen → Auckland → New York → Honolulu → Copenhagen:
| # | Sampled (UTC) | Device zone (`Intl`) | `Intl` offset | **`Date` offset** | `Intl` local time | **`Date` says** | Agree? |
|---|---|---|---|---|---|---|---|
| 1 | 15:22:37 | Europe/Copenhagen | +120 | +120 | 15 Oct 17:22 | Thu Oct 15 17:22 GMT+0200 | YES |
| 2 | 15:24:04 | Pacific/Auckland | +780 | **+120** | **16 Oct** 04:24 | Thu Oct 15 17:24 GMT+0200 | **NO** |
| 3 | 15:24:58 | America/New_York | −240 | **+120** | 15 Oct 11:24 | Thu Oct 15 17:24 GMT+0200 | **NO** |
| 4 | 15:27:10 | Pacific/Honolulu | −600 | **+120** | 15 Oct 05:27 | Thu Oct 15 17:27 GMT+0200 | **NO** |
| 5 | 15:28:10 | Europe/Copenhagen | +120 | +120 | 15 Oct 17:28 | Thu Oct 15 17:28 GMT+0200 | YES |
**And on iOS with React Native 0.88.0** (iPhone 17e, physical device, Release build,
16 October 2026), one process throughout, same sequence:
| # | Sampled (UTC) | Device zone (`Intl`) | `Intl` offset | **`Date` offset** | `Intl` local time | **`Date` says** | Agree? |
|---|---|---|---|---|---|---|---|
| 1 | 06:48:28 | Europe/Copenhagen | +120 | +120 | 16 Oct 08:48 | Fri Oct 16 08:48 GMT+0200 | YES |
| 2 | 06:48:57 | Pacific/Auckland | +780 | **+120** | 16 Oct 19:48 | Fri Oct 16 08:48 GMT+0200 | **NO** |
| 3 | 06:49:32 | America/New_York | −240 | **+120** | 16 Oct 02:49 | Fri Oct 16 08:49 GMT+0200 | **NO** |
| 4 | 06:49:51 | Pacific/Honolulu | −600 | **+120** | **15 Oct** 20:49 | Fri Oct 16 08:49 GMT+0200 | **NO** |
| 5 | 06:50:14 | Europe/Copenhagen | +120 | +120 | 16 Oct 08:50 | Fri Oct 16 08:50 GMT+0200 | YES |
The iPhone's own status-bar clock shows the correct local time in the same screenshot — **19.48**
in Auckland, **02.49** in New York, **20.49** in Honolulu — while `Date` reports 08:48–08:49. The
operating system knows the time; only the JavaScript engine is stuck. Row 4 is a wrong calendar
date in the other direction: still 15 October in Honolulu, `Date` says the 16th.
**And on Android with React Native 0.88.0** (Galaxy S24, 16 October 2026, same sequence):
| # | Sampled (UTC) | Device zone (`Intl`) | `Intl` offset | **`Date` offset** | `Intl` local time | **`Date` says** | Agree? |
|---|---|---|---|---|---|---|---|
| 1 | 07:19:16 | Europe/Copenhagen | +120 | +120 | 16 Oct 09:19 | Fri Oct 16 09:19 GMT+0200 | YES |
| 2 | 07:19:48 | Pacific/Auckland | +780 | **+120** | 16 Oct 20:19 | Fri Oct 16 09:19 GMT+0200 | **NO** |
| 3 | 07:20:19 | America/New_York | −240 | **+120** | 16 Oct 03:20 | Fri Oct 16 09:20 GMT+0200 | **NO** |
| 4 | 07:20:43 | Pacific/Honolulu | −600 | **+120** | **15 Oct** 21:20 | Fri Oct 16 09:20 GMT+0200 | **NO** |
| 5 | 07:21:00 | Europe/Copenhagen | +120 | +120 | 16 Oct 09:21 | Fri Oct 16 09:21 GMT+0200 | YES |
On both platforms, `Date` stays on the starting zone's +120 throughout. In the 0.87.1 run, row 2 is
already 16 October in Auckland while `Date` says the 15th; in the 0.88.0 runs, row 4 is still
15 October in Honolulu while `Date` says the 16th. Row 5 only "agrees" because the device returned
to the zone `Date` was stuck in — everything recorded in between used the wrong offset.
Calling `IHermes::resetTimezoneCache()` from the native resume hook and from the system
zone-change notification (`NSSystemTimeZoneDidChangeNotification` /
`Intent.ACTION_TIMEZONE_CHANGED`) fixes it: offset `120 → 600` on both devices. We have run that
in production since August 2026.
MANDATORY Reproducer
https://github.com/renelaerke/rn-hermes-timezone-reset-repro
Screenshots and Videos
React Native 0.88.0, Copenhagen → Auckland → New York → Honolulu → Copenhagen, one process each.
iOS — iPhone 17e (note the status-bar clock vs Date):
| Copenhagen | Auckland | New York | Honolulu | Copenhagen |
|---|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
Android — Galaxy S24:
| Copenhagen | Auckland | New York | Honolulu | Copenhagen |
|---|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |










Activity
troZee commentedon Oct 16, 2026
Here you can find a fix for that https://github.com/callstack/timezone-hermes-fix
spyshower commentedon Oct 17, 2026
Impressively detailed issue.
Perhaps even more impressive is that not only the issue is known, but Callstack already has a (seemingly) production ready and well maintained repo for this issue specifically.