How Daylight Saving Time Works
Daylight Saving Time (DST) is one of the most common sources of subtle, hard-to-reproduce bugs in software applications. These bugs often only manifest twice a year, making them incredibly difficult to catch during development and testing.
The main problem
Not All Days Are 24 Hours!
The fundamental assumption that "a day is always 24 hours" is wrong in most timezones that observe DST.
// This assumption is dangerous in calendar apps
const tomorrow = Date.now() + 24 * 60 * 60 * 1000; // ❌ Wrong during DST transitions
console.log("tomorrow", tomorrow);
During DST transitions:
- Spring forward: Days are 23 hours long (lose 1 hour)
- Fall back: Days are 25 hours long (gain 1 hour)
The "Twice a Year" Bug
DST bugs are particularly insidious because they only happen during the two DST transition periods each year. This means:
- Your tests might pass 363 days of the year
- Your application works fine in production most of the time
- But twice a year, a critical bug, might happen: A calendar event is set to the wrong time.
Production nightmare: DST bugs often appear suddenly in production, typically in the middle of the night when you are asleep.
Common DST Bug Examples
Spring Forward (Losing an Hour)
On March 10, 2024, in the America/New_York
timezone:
// Demonstrates how naive duration calculations ignore the "skipped" hour when Daylight Saving Time starts in New York (America/New_York).
// 1:59 AM EST (UTC-5) — just before the clocks spring forward
const before = new Date("2024-03-10T01:59:00-05:00");
// 3:00 AM EDT (UTC-4) — the next valid local time after the DST jump
const after = new Date("2024-03-10T03:00:00-04:00");
// How many minutes does JavaScript think have passed?
const diffMinutes = (after.getTime() - before.getTime()) / 60_000;
console.log("diffMinutes", diffMinutes); // 👉 1 minute (!)
What went wrong?*
- Two local timestamps span the DST “spring-forward” gap: “2024-03-10 01:59” (EST, UTC-5) is immediately followed by “2024-03-10 03:00” (EDT, UTC-4); the entire 2 o’clock hour never occurs.
- A naive subtraction therefore returns 1, under-reporting the elapsed wall-clock time by an hour. 🐞 Bug: 61 calendar minutes passed on the wall clock, but our application reports only 1 minute
Fall Back (Gaining an Hour)
On November 3, 2024, in the America/New_York
timezone:
// Demonstrates how naive duration calculations mis-handle the repeated hour when Daylight Saving Time ends in New York (America/New_York).
// 1:30 AM EDT (UTC-4) — before the clocks "fall back"
const first = new Date("2024-11-03T01:30:00-04:00");
// 1:30 AM EST (UTC-5) — the *second* time this wall-clock moment occurs
const second = new Date("2024-11-03T01:30:00-05:00");
// How many minutes does JavaScript think separate these two moments?
const diffMinutes = (second.getTime() - first.getTime()) / 60_000;
console.log("diffMinutes", diffMinutes); // 👉 60 minutes (!)
What went wrong?*
- Two wall-clock timestamps look identical: “2024-11-03 01:30” in New York occurs twice—once as EDT (UTC-4) and again an hour later as EST (UTC-5).
- A substracting them returns 60, even though, from a local-time perspective, you expected the difference between identical wall-clock moments to be 0. 🐞 Bug: The calculations ignores the DST “fall-back” hour silently introducing a one-hour error.
Different timezones on browser and server
It get's worse when your server and users are in different timezones.
import { addDays } from "date-fns";
// Client-side (Singapore, no DST) — user selects "2024-03-09 18:00" for an appointment
// Value sent to API *with* timeZone info:
const clientDateString = "2024-03-09T18:00:00+08:00";
// Server-side (New York). This machine's local timeZone observes DST.
const parsedOnServer = new Date(clientDateString);
// Add *one calendar day* (what the client expects)
const oneDayLater = addDays(parsedOnServer, 1);
console.log("parsedOnServer", parsedOnServer.toString()); // 👉 Sat Mar 09 2024 05:00:00 EST
console.log("oneDayLater", oneDayLater.toString()); // 👉 Sun Mar 10 2024 06:00:00 EDT (!!)
What went wrong?*
- New York moved clocks forward at 2 AM on 2024-03-10, making the day only 23 hours long.
- addDays adds exactly 24 hours, so the wall-clock time shifts forward by +1 hour.
- The client expected 18:00 SGT on the next day, but would see 19:00 when the server response is converted back.
🐞 Bug summary: Even with an explicit timezone offset in the input, server-side date math that relies on the server's local timezone can silently drift during DST transitions.
How Datezone Handles DST
Datezone is always explicit about the timezone when you are doing Date & Time operations.
Every operation that is prone to DST bugs have a timezone
parameter that is required.
This might feel overkill at first but it sets you as a developer up for success and hels you avoid DST bugs.
Always ask yourself: In which Timezone am I doing this calendar calculation?:
import { addDays } from "datezone";
// Spring forward in New York (2024-03-10 2:00 AM -> 3:00 AM)
const beforeDST = 1709999940000; // 2024-03-10 01:59 EST
const _afterDST = addDays(beforeDST, 1, "America/New_York");
// afterDST is 1710082740000 which is 2024-03-11 01:59 EDT
Correct Datezone uses IANA Timezones and knows when to do the DST transitions accordingly
import { addHours } from "datezone";
// Spring forward in New York (2024-03-10 2:00 AM -> 3:00 AM)
const beforeDST = 1710000000000; // 2024-03-10 01:30 EST
const _afterDST = addHours(beforeDST, 1);
// afterDST is 1710003600000 which is 2024-03-10 02:30 EST
Correct Notice how we added one hour, but the clock jumps from 1:30 to 3:30, skipping the nonexistent 2 AM hour due to DST.
Safe Calendar Date Arithmetic
import {
addDays,
calendarToTimestamp,
format,
intervalToDuration,
} from "datezone";
const timeZone = "America/New_York";
// ✅ This correctly handles DST transitions
const today = calendarToTimestamp(
{
day: 10,
hour: 1,
minute: 30,
month: 3,
year: 2024,
},
"America/New_York",
);
// Spring forward transition
const tomorrow = addDays(today, 1, "America/New_York");
// Get the actual difference in hours
const diffInHours = (tomorrow - today) / 1000 / 60 / 60;
const calendarDiff = intervalToDuration(today, tomorrow, timeZone);
console.log("Difference in hours:", diffInHours); // 23
console.log("Calendar Difference", calendarDiff); // { day: 1, hour: 0 }
console.log("Today:", format(today, "yyyy-MM-dd HH:mm z", { timeZone })); // 2024-03-10 01:30 GMT-5
console.log("Tomorrow:", format(tomorrow, "yyyy-MM-dd HH:mm z", { timeZone })); // 2024-03-11 01:30 GMT-4
// On DST transition days:
// - Spring forward: tomorrow is 23 hours later
// - Fall back: tomorrow is 25 hours later
// - But it's always "1 day later" in terms of calendar date
Correct Here we simulate a spring forward transition in New York.
- From 2024-03-10 01:30 GMT-5 to 2024-03-11 01:30 GMT-4
- The difference in actual hours is 23 hours
- But on the Calendar it's 1 day and 0 hours later.
Timezone-Sensitive Operations
Some operations break not only during DST transitions. but all the time.
You want start of day?
Ok but start of day where? Is it start of day in Tokio or in New York? This is very critical information for your application.
Datezone
forces you to Specify a timezone if you want start of day, end of day etc.
import { calendarToTimestamp, endOfDay, format, startOfDay } from "datezone";
// Beginning of the year 2025 in UTC
const timestamp = calendarToTimestamp(
{
day: 1,
hour: 0,
minute: 0,
month: 1,
year: 2025,
},
"UTC",
);
const startOfDayInNewYork = startOfDay(timestamp, "America/New_York");
const endOfDayInNewYork = endOfDay(timestamp, "America/New_York");
const startOfDayInTokio = startOfDay(timestamp, "Asia/Tokyo");
const endOfDayInTokio = endOfDay(timestamp, "Asia/Tokyo");
console.log(
"Start of Day in New York:",
format(startOfDayInNewYork, "yyyy-MM-dd HH:mm z", {
timeZone: "America/New_York",
}),
); // 2024-12-31 00:00 GMT-5
console.log(
"End of Day in New York:",
format(endOfDayInNewYork, "yyyy-MM-dd HH:mm z", {
timeZone: "America/New_York",
}),
); // 2024-12-31 23:59 GMT-5
console.log(
"Start of Day in Tokio:",
format(startOfDayInTokio, "yyyy-MM-dd HH:mm z", {
timeZone: "Asia/Tokyo",
}),
); // 2025-01-01 00:00 GMT+9
console.log(
"End of Day in Tokio:",
format(endOfDayInTokio, "yyyy-MM-dd HH:mm z", {
timeZone: "Asia/Tokyo",
}),
); // 2025-01-01 23:59 GMT+9
DST Best Practices
1. Always Specify Timezones
Having a system the behaves differently depending where your user is based or where your server is deployed is bad. This leads to hard to find bugs in production that you can't reproduce locally.
By always being explicit with timezonen in your application makes your code predictable and easy to test.
2. Use Calendar Arithmetic, Not Time Arithmetic
Before doing any date & time editing. Think twice: Do I want to move forward x amount of milliseconds or do I want to jump to the next day?. In most cases you want to use Calendar Arithmetic when building user facing applications. Humans think in calendar dates, not timestamps.
3. Store in timestamps
Calendar Arithmetic is good for interacting with the user but it's prone to timezone errors. Always store your date and time in Timestamps that are universial.
DST Around the World
DST rules vary significantly by country and region:
- Northern Hemisphere: Spring forward in March/April, fall back in October/November
- Southern Hemisphere: Opposite schedule (spring forward in September/October)
- No DST: Many countries/regions don't observe DST at all
- Different Rules: Some countries have unique DST schedules
IANA Timezone Database: Datezone uses the IANA timezone database, which contains the complete history and future predictions of DST rules for all timezones worldwide.
Summary
DST transitions are a major source of bugs in date-handling code. By understanding how DST works and using timezone-aware functions like those provided by datezone, you can avoid these pitfalls entirely.
Key takeaway: Never assume a day is 24 hours. Always use calendar-based arithmetic with explicit timezone handling for reliable date operations.
Next, learn about best practices for building robust date and time handling in your applications.