Why is handling dates and time zones so hard?
Because time is a political and cultural construct, not a physical constant. Almost every intuitive assumption a programmer makes about dates turns out to be false somewhere, and the failures are silent.
The assumptions that break:
A day has 24 hours. Not on daylight saving transitions, where a day has 23 or 25 — and some regions shift by 30 minutes.
Every local time exists exactly once. In a spring-forward transition, an hour does not exist at all; in autumn, an hour occurs twice, and a timestamp within it is genuinely ambiguous.
Time zones are geographic. They are legal jurisdictions, with boundaries that follow politics, and there are offsets of 45 minutes as well as 30.
Time zone rules are fixed. Governments change them with little notice, sometimes weeks. The IANA time zone database is updated several times a year, and a system running an old copy produces wrong answers with no error — the most insidious failure in this whole area.
Offsets identify a zone. They do not: many zones share an offset and differ in when they change. Storing "+01:00" loses the information needed to compute a future local time.
UTC always moves forward evenly. Leap seconds exist, and some systems smear them.
The rules that actually work:
Store instants in UTC with a timestamp type, and convert at the edges for display.
But store future events as local time plus a zone name — "09:00 Europe/London" — because a meeting scheduled for next year must move if the rules change. Converting a future appointment to UTC and storing that is a real and common bug.
Use zone identifiers, never offsets or abbreviations. "CST" is ambiguous between multiple zones.
Keep the tz database current across every service, container and dependency.
Never do date arithmetic on epoch seconds — use a library that understands calendars.
Test around transitions deliberately, and remember that calendars vary, weeks start on different days, and not every culture uses the Gregorian calendar.