<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Michael Maitland</title>
    <description>The latest articles on DEV Community by Michael Maitland (@mike-mait).</description>
    <link>https://dev.to/mike-mait</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3904470%2F4c3b7505-40b3-48ed-a95a-e0c1977ab7ae.png</url>
      <title>DEV Community: Michael Maitland</title>
      <link>https://dev.to/mike-mait</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mike-mait"/>
    <language>en</language>
    <item>
      <title>The timezone bugs you don't know you have (and how to find them)</title>
      <dc:creator>Michael Maitland</dc:creator>
      <pubDate>Wed, 29 Apr 2026 13:48:16 +0000</pubDate>
      <link>https://dev.to/mike-mait/the-timezone-bugs-you-dont-know-you-have-and-how-to-find-them-gd5</link>
      <guid>https://dev.to/mike-mait/the-timezone-bugs-you-dont-know-you-have-and-how-to-find-them-gd5</guid>
      <description>&lt;p&gt;A user reports that a meeting fired an hour late. You check the database — the timestamp looks fine. You check the UI — the display matches what the user entered. Then you realize: today was DST transition day.&lt;/p&gt;

&lt;p&gt;This post is about the timezone bugs that 90% of production apps have and don't know about. None of these are exotic. All of them are quietly corrupting data in some app you've shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #1: The Spring-Forward Gap
&lt;/h2&gt;

&lt;p&gt;On March 8, 2026 in &lt;code&gt;America/New_York&lt;/code&gt;, clocks jump from 1:59:59 directly to 3:00:00. The entire 2:00–2:59 hour does not exist.&lt;/p&gt;

&lt;p&gt;If a user enters "2:30am" in your scheduler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromISO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-03-08T02:30:00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;America/New_York&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISO&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="c1"&gt;// "2026-03-08T03:30:00.000-04:00"  ← Luxon silently shifted it to 3:30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Luxon "helpfully" resolved the invalid time forward. Your user wanted 2:30am. You stored 3:30am. They have no idea. The bug surfaces 8 months later when their recurring event fires an hour late.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; explicitly check whether a parsed datetime equals what you asked for, and reject (or escalate to the user) if it doesn't. Don't trust your library to "just handle it."&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #2: The Fall-Back Overlap
&lt;/h2&gt;

&lt;p&gt;On November 1, 2026 in &lt;code&gt;America/New_York&lt;/code&gt;, clocks go from 1:59:59 back to 1:00:00. The 1:00–1:59 hour happens twice. "1:30am" is genuinely ambiguous.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromISO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-11-01T01:30:00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;America/New_York&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// -240 (EDT) — but it could just as legitimately be -300 (EST)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most libraries pick one without telling you. Picking "the first occurrence" is a real business decision (your user might mean the second). It should not be a silent default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; when you detect an ambiguous local time, force a policy decision in your domain layer. Log it. Don't let the library guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #3: Non-Whole-Hour Offsets
&lt;/h2&gt;

&lt;p&gt;Pop quiz — what's the offset for &lt;code&gt;Asia/Kathmandu&lt;/code&gt;? Most developers say "+5:30." Wrong, that's India.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nepal: &lt;strong&gt;UTC+5:45&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Chatham Islands: &lt;strong&gt;UTC+12:45&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Lord Howe Island uses a &lt;strong&gt;30-minute&lt;/strong&gt; DST shift, not 60&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your code anywhere does &lt;code&gt;offset / 60&lt;/code&gt; to get hours, or assumes the minutes component is always 0 or 30, you have a bug. It might never fire, because you might never have a Nepali user. Until you do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; treat offsets as minutes-from-UTC, never as hours. Format defensively. Test with Kathmandu in your suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #4: Historical Zone Rules
&lt;/h2&gt;

&lt;p&gt;Germany on 1945-05-24 had a 23:00 → 01:00 jump (the occupation forces synchronized clocks). Russia in 2011 abolished DST permanently. Iran abolished DST in 2022. Egypt re-instated it in 2023. Samoa literally skipped December 30, 2011 to switch date lines.&lt;/p&gt;

&lt;p&gt;If you're storing or computing historical timestamps — billing periods that started years ago, audit trails, IoT data from old devices — your results may be wrong by an hour for any user in an affected zone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; if you're computing across years, pin your tzdata version explicitly and document it. Don't assume the runtime's bundled tzdata is up to date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug #5: tzdb Update Drift
&lt;/h2&gt;

&lt;p&gt;The IANA tzdata gets updated 3-6 times per year. Every release fixes historical errors and adds new rule changes (countries change DST policy all the time — Mexico in 2022, Egypt in 2023, BC's pending change in 2026).&lt;/p&gt;

&lt;p&gt;Your Node.js version was built with a snapshot of tzdata from whenever it was released. If you're on Node 20 from 2023, you're on tzdata from 2023. You're missing every IANA update since then.&lt;/p&gt;

&lt;p&gt;This means &lt;strong&gt;your app gives wrong answers for any zone whose rules changed after your Node version's release date&lt;/strong&gt;. Often by an hour, often silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; decouple tzdata from your runtime version. Use a userland tzdb package you can update independently. I just did this for my production system and wrote up the migration if you want details.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to test for these in your own code
&lt;/h2&gt;

&lt;p&gt;For each of the bugs above, here's a one-liner test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bug #1 detection&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isValidLocalDatetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromISO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;zone&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// Compare requested local time vs what the lib gave back&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yyyy-MM-dd'T'HH:mm:ss&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Bug #2 detection&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isAmbiguousLocalDatetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromISO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;zone&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;before&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run these against your top 50 customers' time zones for the next 5 years worth of DST transition dates. If you find a bug, congrats — you found one that probably already cost you a support ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  My take
&lt;/h2&gt;

&lt;p&gt;I got tired of writing this code in every project. So I built &lt;a href="https://chronoshieldapi.com" rel="noopener noreferrer"&gt;ChronoShield API&lt;/a&gt; — a REST API that handles all of these cases as a service. Today's launch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;npm install chronoshield&lt;/code&gt; / &lt;code&gt;pip install chronoshield&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Free tier: 1k req/mo&lt;/li&gt;
&lt;li&gt;Currently serving IANA tzdata 2026b (latest), with the BC permanent-DST projection already live&lt;/li&gt;
&lt;li&gt;Endpoints: &lt;code&gt;/validate&lt;/code&gt;, &lt;code&gt;/resolve&lt;/code&gt;, &lt;code&gt;/convert&lt;/code&gt;, &lt;code&gt;/version&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the test cases above work whether or not you use the API. If you take nothing else from this post — write the tests. They'll find bugs you didn't know you had.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>api</category>
    </item>
  </channel>
</rss>
