iamkun / iamkun/dayjs

dayjs.tz(ms, 'UTC').startOf('day') returns prior-day UTC midnight on hosts with DST

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

Description

**Describe the bug**

`dayjs.tz(ms, 'UTC').startOf('day')` returns the start of the previous day in UTC instead of the correct day when the host's local timezone has a DST transition on the date being processed.

The Dayjs returned by `dayjs.tz(ms, 'UTC')` is supposed to be pinned to UTC for chained operations, but `startOf('day')` appears to fall back to host-local-time semantics. Using the UTC plugin directly (`dayjs.utc(ms)`) produces the correct result.

**Reproducing test case**

Run with the host's `TZ` set to a zone that has a DST transition on 2024-11-03 (e.g. `TZ=America/Los_Angeles`):

```javascript
const dayjs = require('dayjs');
const utc = require('dayjs/plugin/utc');
const timezone = require('dayjs/plugin/timezone');
dayjs.extend(utc);
dayjs.extend(timezone);

// 2024-11-03 23:00 UTC. Local TZ America/Los_Angeles ends DST at
// 02:00 PDT on this date (PDT -> PST), so 2024-11-03 straddles a
// DST transition in local time.
const ms = new Date('2024-11-03T23:00:00Z').getTime();

const buggy = dayjs.tz(ms, 'UTC').startOf('day').valueOf();
const correct = dayjs.utc(ms).startOf('day').valueOf();

const expected = new Date('2024-11-03T00:00:00Z').getTime();

console.log('buggy ->', new Date(buggy).toISOString());
console.log('correct ->', new Date(correct).toISOString());
console.log('expected->', new Date(expected).toISOString());

// Expected output:
// buggy -> 2024-11-03T00:00:00.000Z
// correct -> 2024-11-03T00:00:00.000Z
// expected-> 2024-11-03T00:00:00.000Z
//
// Actual output (on a host with US DST on 2024-11-03):
// buggy -> 2024-11-02T23:00:00.000Z <-- off by one hour
// correct -> 2024-11-03T00:00:00.000Z
// expected-> 2024-11-03T00:00:00.000Z
```

**Expected behavior**

`dayjs.tz(ms, 'UTC').startOf('day').valueOf()` should equal `dayjs.utc(ms).startOf('day').valueOf()` — i.e., the start of the UTC day containing `ms`. The choice of host-local timezone should not affect the result when the chained timezone is explicitly UTC.

**Additional notes**

- The bug only reproduces when the host's local timezone has a DST transition on the date under test. UTC hosts and non-DST hosts hide it.
- Other IANA-named zones (`US/Pacific`, `US/Eastern`, `Europe/Berlin`, etc.) work correctly even on DST host dates — sweeping a fix-test matrix across `US/Pacific`, `US/Eastern`, `US/Hawaii`, and `UTC` on 2024-11-03, only the UTC case fails.
- Symptom in real code: a day-by-day boundary walk where each iteration computes `dayjs.tz(watermark, 'UTC').startOf('day').add(1, 'day')` gets pinned at a single moment, because `startOf('day')` returns the prior day's midnight and `+1 day` lands exactly on `watermark`.
- The `.startOf('millisecond')` workaround that fixes #2957 does **not** fix this bug — confirmed by direct probing. The two issues share a "chained operations don't respect `.tz()`" root cause but they hit different internal code paths.

**Information**
- Day.js Version: 1.11.21
- OS: macOS 25.4.0
- Browser: Node 24.16.0
- Time zone: `America/Los_Angeles` (host); requested zone in the buggy call: `UTC`

Contributor guide

Open the contributing guide

Research direction

Start with the UTC and timezone plugin entry points used by `dayjs.tz` and `dayjs.utc`, then reproduce the case with `TZ=America/Los_Angeles` and the supplied 2024-11-03 timestamp. Add a regression test covering `dayjs.tz(ms, 'UTC').startOf('day')` against `dayjs.utc(ms).startOf('day')`; done means both values equal the expected UTC midnight across the stated host zones.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
tooling
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.