Skip to content

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

Description

@bluepnume

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):

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 Inconsistent results on 25 hour day after time change #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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions