<?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: Eugen Taranowski</title>
    <description>The latest articles on DEV Community by Eugen Taranowski (@eugen_taranowski).</description>
    <link>https://dev.to/eugen_taranowski</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1933715%2F6441288c-769e-4712-8056-3dbfbe462a46.jpeg</url>
      <title>DEV Community: Eugen Taranowski</title>
      <link>https://dev.to/eugen_taranowski</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eugen_taranowski"/>
    <language>en</language>
    <item>
      <title>The bug that only happened if you bookmarked the page</title>
      <dc:creator>Eugen Taranowski</dc:creator>
      <pubDate>Mon, 17 Aug 2026 10:31:47 +0000</pubDate>
      <link>https://dev.to/eugen_taranowski/the-bug-that-only-happened-if-you-bookmarked-the-page-1d02</link>
      <guid>https://dev.to/eugen_taranowski/the-bug-that-only-happened-if-you-bookmarked-the-page-1d02</guid>
      <description>&lt;p&gt;I found this by accident, while chasing something else entirely. Testing an unrelated date bug, I loaded the Favorites page directly by URL — and the test data I'd just saved was gone. Not "failed to display". Gone from storage.&lt;/p&gt;

&lt;p&gt;The strange part: clicking through to the same page from inside the app worked perfectly, every time. Same page, same code, same data. The only difference was how you arrived.&lt;/p&gt;

&lt;p&gt;That asymmetry turned out to be the entire explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two reasonable decisions
&lt;/h2&gt;

&lt;p&gt;The first piece is a hydration fix. The app stores your saved shows in &lt;code&gt;localStorage&lt;/code&gt;. The obvious way to load them is to read storage when the state is first created:&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;favorites&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFavorites&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;favorites&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works, until you server-render. The server has no &lt;code&gt;localStorage&lt;/code&gt;, so it renders an empty list. A returning visitor's browser &lt;em&gt;does&lt;/em&gt; have the data, so it renders a full one. React compares the two, finds they disagree, and complains about a hydration mismatch.&lt;/p&gt;

&lt;p&gt;The standard fix — and the one I'd applied earlier — is to stop reading storage during render. Start empty on both server and client so the first render matches, then load the real data in an effect afterwards:&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;favorites&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFavorites&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;

&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;favorites&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setFavorites&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;));&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;The second piece is ordinary product behaviour: when you open Favorites, if the show data hasn't been refreshed in twelve hours, fetch fresh details for each saved show and store the result.&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="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lastRefresh&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;TWELVE_HOURS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;refreshFavorites&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// maps over `favorites`, writes result to localStorage&lt;/span&gt;
  &lt;span class="p"&gt;}&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;Both correct. Both, in isolation, uncontroversial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where they collide
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;React runs effects from the bottom of the tree upward.&lt;/strong&gt; Children first, then their parents.&lt;/p&gt;

&lt;p&gt;When you land directly on the Favorites page, the app's data provider and the page itself mount together, in the same commit. So:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The page's effect runs first. It checks the timestamp, decides a refresh is due, and calls the refresh function.&lt;/li&gt;
&lt;li&gt;That function reads the current list of favorites — which, at this exact moment, is still the empty array everything started as, because the provider's effect (a &lt;em&gt;parent&lt;/em&gt; effect) hasn't run yet.&lt;/li&gt;
&lt;li&gt;So it refreshes a list of zero shows. It receives zero shows back. It writes that result to &lt;code&gt;localStorage&lt;/code&gt;, overwriting whatever was there.&lt;/li&gt;
&lt;li&gt;A moment later the provider's effect runs, reads storage to restore your favorites, and finds an empty array. Because it just was one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The hydration fix wasn't the mistake — it was the right call, and reverting it would just bring back the bug it solved. The mistake was not noticing that deferring the load created a window where the data legitimately isn't there yet, and that something else was already running inside that window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why normal use never showed it
&lt;/h2&gt;

&lt;p&gt;Navigate to Favorites by clicking a link and the provider is already mounted from whatever page you were on. Its effect ran long ago. The favorites are loaded. The page mounts alone, the refresh reads a populated list, and everything works.&lt;/p&gt;

&lt;p&gt;The bug needs the provider and the page to mount in the same commit, which only happens on a fresh load of that specific URL: a bookmark, a refresh while sitting on the page, a link shared from outside, or reopening a tab.&lt;/p&gt;

&lt;p&gt;Which is a genuinely unpleasant profile for a data-loss bug. It skips the path developers use constantly while building — clicking around a running app — and hits the path a returning user is most likely to take. Someone who bookmarks the page they care about is exactly the person with the most to lose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, and a better one from the comments
&lt;/h2&gt;

&lt;p&gt;My first fix was a flag distinguishing "empty" from "not loaded yet":&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hydrated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setHydrated&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&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;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;favorites&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setFavorites&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setHydrated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// runs even if the parse throws&lt;/span&gt;
  &lt;span class="p"&gt;}&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;with the page waiting on it before refreshing.&lt;/p&gt;

&lt;p&gt;That works. But when I posted this, a commenter pointed out it's weaker than it looks: it's a guard every future caller has to remember to check — structurally the same shape as the original bug, &lt;em&gt;correct only as long as nobody forgets&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The stronger version: &lt;strong&gt;make the refresh read storage directly rather than trusting React state.&lt;/strong&gt;&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;refreshFavorites&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;favorites&lt;/span&gt;&lt;span class="dl"&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;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// ...refresh `saved`, write the result back&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt; &lt;span class="c1"&gt;// no dependency on component state at all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now there is no ordering in which the refresh can observe fewer items than are actually saved. The overwrite isn't guarded against — it's impossible. I removed the flag from the write path entirely and re-tested the original failure scenario &lt;em&gt;with the guard gone&lt;/em&gt;, to confirm the safety was structural rather than conditional.&lt;/p&gt;

&lt;p&gt;The same commenter made a second point I'd missed: the empty state was rendering "No favorites added yet" during that same pre-hydration window — telling a returning user their list was gone, a moment before it appeared. "Empty" and "not loaded yet" have to be distinguishable when you &lt;em&gt;render&lt;/em&gt;, too, not just when you write.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take from it
&lt;/h2&gt;

&lt;p&gt;Deferring work to fix one problem creates a window where your state is temporarily untrue. That's fine, as long as nothing else acts during it. Worth asking, whenever you move initialisation into an effect: what else runs before this, and what will it think the state means?&lt;/p&gt;

&lt;p&gt;More generally: "empty" and "not loaded yet" looking identical is a recurring source of this kind of damage. If code can act on the difference, it needs to be able to see the difference.&lt;/p&gt;

&lt;p&gt;And the reason I found it at all is that I loaded a page the way a user would, rather than the way I always did.&lt;/p&gt;

</description>
      <category>react</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Why TMDB says your Apple TV+ show airs a day early</title>
      <dc:creator>Eugen Taranowski</dc:creator>
      <pubDate>Mon, 17 Aug 2026 10:22:43 +0000</pubDate>
      <link>https://dev.to/eugen_taranowski/why-tmdb-says-your-apple-tv-show-airs-a-day-early-3862</link>
      <guid>https://dev.to/eugen_taranowski/why-tmdb-says-your-apple-tv-show-airs-a-day-early-3862</guid>
      <description>&lt;p&gt;A user reported that my TV tracker was showing the wrong air date for &lt;em&gt;Silo&lt;/em&gt;. Their favourites card said the next episode aired today. Google said tomorrow. One of us was wrong, and the obvious assumption was that it was me.&lt;/p&gt;

&lt;p&gt;It was — but not for the reason I first thought, and the underlying cause is worth writing down, because it affects any app built on the same data.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, a genuine bug of my own
&lt;/h2&gt;

&lt;p&gt;The initial report was that the same episode showed different countdowns in different places in the app: a show card said "airs tomorrow" while the notification for the same episode said "airs today".&lt;/p&gt;

&lt;p&gt;That one was mine. Two different pieces of code were deciding what "today" meant in two different ways. One compared calendar days entirely within the show's own country's timezone. The other took the show's end-of-day cutoff, converted that instant into the &lt;em&gt;viewer's&lt;/em&gt; local timezone, and compared calendar days there.&lt;/p&gt;

&lt;p&gt;Those two approaches agree only when the show's timezone and the viewer's are close together. For a US show viewed from Germany — a nine-hour gap — they routinely land on different calendar days, shifting the whole today/tomorrow boundary depending on what time of day you happened to look.&lt;/p&gt;

&lt;p&gt;I fixed it by making both read from one shared function that only ever counts days in a single timezone. The two displays then agreed with each other perfectly.&lt;/p&gt;

&lt;p&gt;They were also both still wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that wasn't my code
&lt;/h2&gt;

&lt;p&gt;With the app now internally consistent, &lt;em&gt;Silo&lt;/em&gt; still said "airs today" for an episode that Apple, and everyone reporting on it, placed the following day. So I checked the source data against published release dates.&lt;/p&gt;

&lt;p&gt;Like a great many TV apps, this one gets show data from &lt;a href="https://www.themoviedb.org/" rel="noopener noreferrer"&gt;TMDB&lt;/a&gt;. Here's what its &lt;code&gt;air_date&lt;/code&gt; field said for two consecutive &lt;em&gt;Silo&lt;/em&gt; episodes, next to the dates in press coverage:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Episode&lt;/th&gt;
&lt;th&gt;TMDB&lt;/th&gt;
&lt;th&gt;Actually released&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;S3E6 "The Drive"&lt;/td&gt;
&lt;td&gt;Thu 6 Aug&lt;/td&gt;
&lt;td&gt;Fri 7 Aug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3E7 "Radio"&lt;/td&gt;
&lt;td&gt;Thu 13 Aug&lt;/td&gt;
&lt;td&gt;Fri 14 Aug&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both exactly one day early. Both a Thursday where the real release was a Friday — matching &lt;em&gt;Silo&lt;/em&gt;'s actual weekly Friday schedule. Two consecutive episodes ruled out a one-off typo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the date is "wrong" on purpose
&lt;/h2&gt;

&lt;p&gt;It isn't a typo. TMDB's convention is that &lt;code&gt;air_date&lt;/code&gt; records the &lt;strong&gt;earliest calendar day an episode becomes available in its country of origin&lt;/strong&gt; — not the date the platform advertises.&lt;/p&gt;

&lt;p&gt;For a traditional broadcaster those are the same day. For a global streaming service they aren't. A TMDB moderator spells it out in their &lt;a href="https://www.themoviedb.org/talk/63eb406c8e870200a988f22d" rel="noopener noreferrer"&gt;"Clarification on Air Date policy" thread&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Amazon Prime release its new content at 0h GMT and Apple TV at 0h Ireland Time on the advertized date, which means that the content is available in the evening of the previous day in the United States. If its a United States show, the recorded date is this previous day and if its an European show, the recorded date is the advertize day."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last sentence is the part worth reading twice, and the part I initially got wrong.&lt;/p&gt;

&lt;p&gt;The shift is &lt;strong&gt;not a property of the service&lt;/strong&gt;. It depends on the show's &lt;em&gt;origin country&lt;/em&gt;: midnight in Ireland is still the previous evening in Los Angeles, but it's simply midnight in London. So a British show on Apple TV+ is recorded under its advertised date, with no gap at all.&lt;/p&gt;

&lt;p&gt;I checked, because my first version of the fix ignored that clause. &lt;em&gt;Slow Horses&lt;/em&gt; is a British series on Apple TV+: TMDB says 29 October, and so does the press. Shifting that one — as my first attempt would have — would have made it a day late. The same bug pointing the other way.&lt;/p&gt;

&lt;h2&gt;
  
  
  A control case, because one pattern isn't a rule
&lt;/h2&gt;

&lt;p&gt;The obvious risk here is over-correcting: deciding "TMDB is a day early" and shifting everything, which breaks every show that was already right.&lt;/p&gt;

&lt;p&gt;So I checked a show releasing the same week on a different service: &lt;em&gt;Stuart Fails to Save the Universe&lt;/em&gt;, on HBO Max. TMDB said 13 August. Press said 13 August. No gap.&lt;/p&gt;

&lt;p&gt;The discrepancy isn't general to TMDB, or to streaming, or even to every show on these services. It needs both conditions at once: a service that releases globally at a single instant, &lt;strong&gt;and&lt;/strong&gt; an origin country far enough west that the instant lands on the previous local day.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reader's example, and the detail that explains it
&lt;/h2&gt;

&lt;p&gt;After this went up, a commenter pointed out the same thing with the &lt;em&gt;Ted Lasso&lt;/em&gt; season four premiere — a cleaner example than mine, because the advertised date comes from Apple directly.&lt;/p&gt;

&lt;p&gt;TMDB has the episode on &lt;strong&gt;4 August&lt;/strong&gt;. &lt;a href="https://www.apple.com/tv-pr/news/2026/04/apple-tvs-emmy-award-winning-global-smash-hit-series-ted-lasso-returns-for-season-four-on-wednesday-august-5/" rel="noopener noreferrer"&gt;Apple's own press release&lt;/a&gt; says the season returns &lt;strong&gt;Wednesday 5 August&lt;/strong&gt;. It went live at 9pm Eastern on the 4th — the night before the date Apple itself advertised.&lt;/p&gt;

&lt;p&gt;The same commenter mentioned something that turns out to be the root of the whole problem: &lt;strong&gt;TMDB records a date, with no time attached.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's why this can't be resolved inside TMDB. "4 August" is a complete answer if you also know the release happened at 9pm Eastern; it's ambiguous if you don't. A timestamp would let every consumer convert correctly for its own audience. A bare date forces each of them to reconstruct the missing hours — which is exactly the reconstruction this post is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually did
&lt;/h2&gt;

&lt;p&gt;The app now reads a show's network and its origin country together. The date is shifted forward to the advertised release only when the service is one of these two &lt;strong&gt;and&lt;/strong&gt; the origin country sits behind Irish time — decided by comparing the two timezones, rather than by keeping a list of countries.&lt;/p&gt;

&lt;p&gt;Concretely: &lt;em&gt;Silo&lt;/em&gt; (American) shifts. &lt;em&gt;Slow Horses&lt;/em&gt; (British) doesn't, despite both being Apple TV+ originals released the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you build on TMDB
&lt;/h2&gt;

&lt;p&gt;Three things worth taking away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;air_date&lt;/code&gt; means "earliest availability in the origin country"&lt;/strong&gt;, not "the date this is advertised as". Most of the time there's no difference. For globally-released streaming shows there is, and it will look like your app is broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The correction is narrower than it first appears.&lt;/strong&gt; It would have been easy — and wrong — to shift everything on those two services. Half the fix is knowing when &lt;em&gt;not&lt;/em&gt; to apply it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agreement isn't correctness.&lt;/strong&gt; I fixed a real inconsistency in my own code, the two displays started agreeing with each other, and that felt like success. It was only checking against the outside world — the thing the user did, and I hadn't — that surfaced the actual problem. Then I repeated the mistake in miniature by shipping a network-only rule without checking a British show against it.&lt;/p&gt;

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