iamkun / iamkun/dayjs

Unstable timezone parsing with dayjs.tz()

Open
#2,891 6 comments 8 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
48.7k
Forks
2.5k
PR merge metrics
No merged PRs in 30d

Description

### **Bug Report: `dayjs.tz()` produces incorrect and inconsistent results on React Native iOS with Hermes**

- **Package Name and Version:** `dayjs` v1.11.13 (or latest)
- **React Native / Expo Version:** React Native v0.76.9+ (with New Architecture/Fabric), Expo SDK 52.0.46+
- **OS and Environment:** iOS 18.2 iPhone 13 Device and iPhone 16 Pro iOS 18.2 Simulator

#### Step 1: Describe the problem

When parsing a timezone-naive date string with an IANA timezone name (e.g., `Asia/Seoul`), `dayjs.tz()` produces an incorrect and highly inconsistent result **only on some React Native iOS environments**. The same code works perfectly on Android and in web browsers.

The most critical symptom is that **the parsed result changes over time**. For the exact same input string, the `minutes` part of the parsed `Date` object changes as the device's real-world clock ticks. This suggests that the parsing logic is being "contaminated" by the current system time.

**Example of the bug:**
- **Input:** `dayjs.tz("2025-06-16T14:07:00", "Asia/Seoul")`
- **Expected UTC ISO String:** `2025-06-16T05:07:00.000Z`
- **Actual UTC ISO String at 10:30 AM:** `2025-06-16T05:16:00.000Z` (Incorrect hour and minute)
- **Actual UTC ISO String at 10:31 AM:** A different, also incorrect value. The `minutes` part keeps changing.

This issue persists even when the `customParseFormat` plugin is used with an explicit format string.

---

#### Step 2: How to reproduce

This bug appears to be specific to the **React Native iOS + Hermes engine** environment, especially when the New Architecture (Fabric) is enabled.

**1. Set up a React Native project with Hermes enabled for iOS.**
A new project created with `npx create-expo-app` and then prebuilt with `npx expo prebuild` can be used. Ensure `hermes_enabled` is `true` in `ios/Podfile`.

**2. Install `dayjs` and its plugins.**
```bash
npm install dayjs
```

**3. Use the following code snippet in any component.**
```jsx
import React, { useState, useEffect } from 'react';
import { View, Text } from 'react-native';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import customParseFormat from 'dayjs/plugin/customParseFormat';

// Extend plugins
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(customParseFormat);

const BugReproductionComponent = () => {
const [parsedTime, setParsedTime] = useState(null);

const dateString = "2025-06-16T14:07:00";
const timeZone = "Asia/Seoul";
const format = "YYYY-MM-DDTHH:mm:ss";

const parseDate = () => {
// This is the problematic line
const result = dayjs.tz(dateString, format, timeZone);
setParsedTime(result);
console.log(`[${new Date().toLocaleTimeString()}] Parsed with dayjs.tz:`, result.toISOString());
};

useEffect(() => {
parseDate(); // Initial parse
const interval = setInterval(parseDate, 5000); // Re-parse every 5 seconds
return () => clearInterval(interval);
}, []);

return (

Input String: {dateString}
Timezone: {timeZone}

Expected UTC: 2025-06-16T05:07:00.000Z


Actual UTC: {parsedTime ? parsedTime.toISOString() : 'Parsing...'}


(Check console logs to see the value changing over time)


);
};

export default BugReproductionComponent;
```

**4. Run the app on an iOS Simulator or a physical iOS device.**
Observe the console logs. You will see that the parsed UTC string is incorrect and its value, especially the `minutes`, changes with each new log entry.

---

#### Step 3: Investigation and Suspected Cause

The investigation suggests this is not a simple bug but a deep interaction issue between `dayjs`'s parsing logic and the underlying JavaScript engine's `Intl` API implementation on iOS.

**1. Alternative Library `date-fns-tz` Works Correctly:**
Using `date-fns-tz` with the same input string and IANA timezone name produces the correct and stable result in the exact same environment.
```javascript
import { zonedTimeToUtc } from 'date-fns-tz';

const utcDate = zonedTimeToUtc("2025-06-16T14:07:00", "Asia/Seoul");
console.log(utcDate.toISOString()); // Correctly logs "2025-06-16T05:07:00.000Z"
```
This strongly indicates the issue lies within `dayjs`'s specific implementation of IANA timezone parsing.

**2. A Stable Workaround for `dayjs`:**
The bug can be completely avoided by providing a numeric UTC offset instead of an IANA name. This forces `dayjs` to bypass its problematic timezone resolution logic.
```javascript
const stableResult = dayjs("2025-06-16T14:07:00+09:00");
console.log(stableResult.toISOString()); // Correctly logs "2025-06-16T05:07:00.000Z"
```

**3. Suspected Implementation Difference:**
The core of the problem seems to be how `dayjs` and `date-fns-tz` handle timezone-naive strings.
- **`dayjs`'s Approach (Suspected):** It appears to use an indirect, inference-based algorithm. It might first parse the string using the system's local time, and then ask the native `Intl` API for an offset based on this ambiguous, intermediate `Date` object. This process is susceptible to bugs in the native `Intl` implementation, where the **current system time** contaminates the offset calculation for a **future date**.
- **`date-fns-tz`'s Approach (Suspected):** It seems to use a more robust, direct calculation method. It likely parses the string into pure date components, creates a UTC-based `Date` object, and then uses the `Intl` API in a more constrained way to *calculate* the precise offset for that specific point in time, avoiding any reliance on the current system clock.

---

#### Step 4: Expected behavior

`dayjs.tz("2025-06-16T14:07:00", "Asia/Seoul")` should consistently and correctly produce a `dayjs` object that represents `2025-06-16T05:07:00.000Z` in UTC, regardless of the platform, JavaScript engine, or the current system time. The result should be stable and not fluctuate.

---

#### **Final thoughts and request for a fix**

Considering that `date-fns-tz` handles this case correctly in the same environment and that providing a direct UTC offset bypasses the issue, the evidence strongly points to a defect in `dayjs`'s internal algorithm for parsing timezone-naive strings with IANA zone names.

This bug severely impacts the reliability of `dayjs` for cross-platform development, especially in a modern stack like React Native with the Hermes engine. It can lead to critical, hard-to-debug data inconsistencies.

We kindly request that the `dayjs` team investigate this parsing logic. A fix would be greatly appreciated by the React Native community and would restore confidence in using `dayjs` for time-critical applications. Please let us know if a minimal reproducible repository or any further information would be helpful.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.