DST & the edges
Spring-forward gaps make some local times nonexistent; fall-back overlaps make others ambiguous.
Daylight Saving Time (DST) is the practice of advancing clocks — typically by one hour — during warmer months so that evening daylight lasts longer. When a zone transitions, the offset changes: for example, America/New_York switches between −05:00 (EST) and −04:00 (EDT).
At the spring-forward transition, clocks skip forward: the local clock jumps from, say, 01:59:59 to 03:00:00. The hour from 02:00 to 02:59:59 never occurs on the wall clock. Any local time in that range is nonexistent — it corresponds to no valid instant.
At the fall-back transition, clocks repeat an hour: the local clock runs from 01:59:59 back to 01:00:00. Every local time in the range 01:00:00–01:59:59 occurs twice, once in each offset. Any such time is ambiguous — it corresponds to two different instants.
A zone-aware type makes you choose what happens in the gap instead of guessing:
// Spring-forward 2026-03-08: 02:00 jumps to 03:00, so 02:30 doesn't exist.
Temporal.ZonedDateTime.from(
{ year: 2026, month: 3, day: 8, hour: 2, minute: 30, timeZone: 'America/New_York' },
{ disambiguation: 'reject' }
); // throws RangeError — the offset flips -05:00 (EST) -> -04:00 (EDT)JS examples use Temporal — standardized in ES2026, available via a polyfill today.
from datetime import datetime
from zoneinfo import ZoneInfo
ny = ZoneInfo('America/New_York')
# Python does NOT reject the nonexistent 02:30 — it silently keeps the
# pre-transition offset (a different policy from the 'reject' shown above;
# Temporal's *default* shifts the wall clock forward to the same instant):
datetime(2026, 3, 8, 2, 30, tzinfo=ny).isoformat() # '2026-03-08T02:30:00-05:00'import java.time.*;
// Spring-forward 2026-03-08: 02:00 jumps to 03:00, so 02:30 doesn't exist.
// java.time does NOT throw — ZonedDateTime.of silently shifts forward by the gap:
ZonedDateTime gap = ZonedDateTime.of(
LocalDateTime.of(2026, 3, 8, 2, 30), ZoneId.of("America/New_York"));
gap.toString(); // '2026-03-08T03:30-04:00[America/New_York]'
gap.toInstant().toString(); // '2026-03-08T07:30:00Z' — i.e. 03:30 EDT, not an errorJava examples use java.time (JSR-310) — see the Java page for the legacy java.util.Date minefield.
import kotlinx.datetime.*
// Spring-forward 2026-03-08: 02:00 jumps to 03:00, so 02:30 doesn't exist.
// kotlinx-datetime does NOT throw — it silently shifts forward by the gap:
val gap = LocalDateTime(2026, 3, 8, 2, 30)
gap.toInstant(TimeZone.of("America/New_York"))
.toString() // '2026-03-08T07:30:00Z' — i.e. 03:30 EDT, not an errorKotlin examples use kotlinx-datetime, JetBrains’ library — separate from the stdlib.
import Foundation
// Foundation also resolves the nonexistent 02:30 by silently shifting forward:
var cal = Calendar(identifier: .gregorian)
cal.timeZone = TimeZone(identifier: "America/New_York")!
let gap = DateComponents(year: 2026, month: 3, day: 8, hour: 2, minute: 30)
cal.date(from: gap) // 2026-03-08T07:30:00Z — i.e. 03:30 EDT, no error eitherSwift examples use Foundation’s Calendar/DateComponents — see the Swift page.
import (
"fmt"
"time"
)
// Spring-forward 2026-03-08: 02:00 jumps to 03:00, so 02:30 doesn't exist.
// time.Date does NOT error (it has no error to return); here it resolves to
// the PRE-transition (EST) offset — one hour earlier than asked (Go's docs
// don't guarantee which side of the gap you get):
ny, _ := time.LoadLocation("America/New_York")
gap := time.Date(2026, time.March, 8, 2, 30, 0, 0, ny)
fmt.Println(gap) // 2026-03-08 01:30:00 -0500 EST — resolved, not an error
fmt.Println(gap.UTC()) // 2026-03-08 06:30:00 +0000 UTCGo examples use the standard library’s time package — see the Go page.
use jiff::civil::date;
use jiff::tz::TimeZone;
// Spring-forward 2026-03-08: 02:00 jumps to 03:00, so 02:30 doesn't exist.
// jiff's default resolves the gap forward, like Temporal's 'compatible':
let dt = date(2026, 3, 8).at(2, 30, 0, 0);
println!("{}", dt.in_tz("America/New_York")?); // 2026-03-08T03:30:00-04:00[America/New_York]
// And like Temporal, you can reject it instead of guessing:
let tz = TimeZone::get("America/New_York")?;
tz.to_ambiguous_zoned(dt).unambiguous()?; // Err — datetime is ambiguous: falls in the gapRust examples use jiff, a third-party crate — Rust’s std has no calendar types at all.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// Spring-forward 2026-03-08: 02:00 jumps to 03:00, so 02:30 doesn't exist.
// mktime does NOT error — it silently NORMALIZES and rewrites your struct tm.
// This libc resolves the gap to the PRE-transition (EST) offset — one hour
// earlier than asked (like Go above; the C standard leaves this unspecified):
setenv("TZ", "America/New_York", 1);
tzset();
struct tm t = {0};
t.tm_year = 2026 - 1900; t.tm_mon = 3 - 1; t.tm_mday = 8;
t.tm_hour = 2; t.tm_min = 30; t.tm_isdst = -1;
mktime(&t);
char buf[32];
strftime(buf, sizeof buf, "%Y-%m-%dT%H:%M:%S", &t);
printf("%s\n", buf); // 2026-03-08T01:30:00 — 02:30 silently became 01:30 ESTC examples use <time.h> — see the C page. Standard C has no zone-aware or date-only type; the zone is a process-global set by TZ.
#include <chrono>
#include <format>
#include <iostream>
using namespace std::chrono;
// Spring-forward 2026-03-08: 02:00 jumps to 03:00, so 02:30 doesn't exist.
// You may resolve the gap explicitly with a choose:: policy:
auto gap = local_days{March/8/2026} + 2h + 30min;
zoned_time resolved{"America/New_York", gap, choose::latest};
std::cout << std::format("{:%Y-%m-%dT%H:%M:%S%z}", resolved) << "\n"; // 2026-03-08T03:00:00-0400
// But the DEFAULT construction REJECTS the nonexistent time — it throws
// nonexistent_local_time rather than silently guessing a side of the gap:
zoned_time strict{"America/New_York", gap}; // throws nonexistent_local_time
std::cout << std::format("{}", strict) << "\n";C++ examples use std::chrono (C++20) — see the C++ page. The pre-C++20 world lived on C’s <time.h>.
Consequences for software
Code that constructs a local datetime directly (e.g. “create a datetime for 2026-03-08 02:30 in America/New_York”) must have a policy for gaps (raise an error? shift forward? shift backward?) and for overlaps (pick the earlier instant? the later? require explicit disambiguation). Different libraries make different default choices, and many do not document them clearly.
Pitfall: Assuming every local calendar day is exactly 24 hours long. In zones that observe DST, the spring-forward day is 23 hours and the fall-back day is 25 hours. Code that calculates “end of day” as start_of_day + 86400 seconds will be wrong on those two days each year.
Pitfall: Scheduling a recurring job at a fixed local time (e.g. “run at 02:30 every day”) without accounting for DST. On the spring-forward day, 02:30 does not exist and the job will either be skipped or fire at an unintended time depending on the scheduler. For the storage and expansion pattern that handles this, see Recurring events.
See also: Time zones vs offsets, Recurring events, and the IANA tz database.
Go deeper: not all DST transitions are ±1 hour at 2 AM
The popular mental model — “clocks go forward one hour at 2 AM in spring” — holds for North America and most of Europe, but it is not universal:
- Some zones transition by 30 minutes rather than 60 (e.g.
Australia/Lord_Howe). (A 45-minute standing offset exists —Asia/Kathmandu,Pacific/Chatham— but no zone makes a 45-minute DST shift.) - Some transitions happen at midnight, noon, or other times.
- Some regions have historically switched DST on and off in irregular ways (wartime, government changes).
- A small number of zones have observed half-hour or quarter-hour offsets from UTC (e.g.
Asia/Kolkatais+05:30,Australia/Lord_Howeshifts between+10:30and+11:00). - Lord Howe Island’s transition is only 30 minutes, so the gap/overlap is only half an hour wide.
The only reliable source of truth is the IANA Time Zone Database, updated to reflect legislative changes. See also Time zones vs offsets for why storing the zone name — not just the offset — is necessary to handle these correctly.