<?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: Hammad Shams Uddin</title>
    <description>The latest articles on DEV Community by Hammad Shams Uddin (@hammad4june1999).</description>
    <link>https://dev.to/hammad4june1999</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%2F4066514%2F2ba9ed94-45a3-4e62-910f-8c42551b2b91.jpg</url>
      <title>DEV Community: Hammad Shams Uddin</title>
      <link>https://dev.to/hammad4june1999</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hammad4june1999"/>
    <language>en</language>
    <item>
      <title>You cannot round three numbers that have to add up to a fourth</title>
      <dc:creator>Hammad Shams Uddin</dc:creator>
      <pubDate>Mon, 24 Aug 2026 19:36:19 +0000</pubDate>
      <link>https://dev.to/hammad4june1999/you-cannot-round-three-numbers-that-have-to-add-up-to-a-fourth-8jh</link>
      <guid>https://dev.to/hammad4june1999/you-cannot-round-three-numbers-that-have-to-add-up-to-a-fourth-8jh</guid>
      <description>&lt;p&gt;A macro calculator takes a calorie target and splits it into grams of carbs, protein and fat. Mine did this:&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;carb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cals&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&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;prot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cals&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;split&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="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&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;fat&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cals&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prot&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; / &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;carb&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; / &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fat&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; g`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask it for 2,000 kcal on a 40/30/30 split and it says &lt;strong&gt;150 / 200 / 67 g&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Multiply that back out — protein and carbs are 4 kcal a gram, fat is 9:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;200 × 4  +  150 × 4  +  67 × 9  =  2,003
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You asked for 2,000. It is not a big miss. It is a miss on &lt;strong&gt;the one thing the tool exists to do&lt;/strong&gt;, and any user with a phone can find it, because adding up to the number you typed is the entire point of a split.&lt;/p&gt;

&lt;p&gt;It was not one unlucky input either:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;target&lt;/th&gt;
&lt;th&gt;plan&lt;/th&gt;
&lt;th&gt;it said&lt;/th&gt;
&lt;th&gt;that is actually&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2,000&lt;/td&gt;
&lt;td&gt;balanced&lt;/td&gt;
&lt;td&gt;150 / 200 / 67&lt;/td&gt;
&lt;td&gt;2,003&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2,000&lt;/td&gt;
&lt;td&gt;keto&lt;/td&gt;
&lt;td&gt;125 / 25 / 156&lt;/td&gt;
&lt;td&gt;2,004&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,750&lt;/td&gt;
&lt;td&gt;low carb&lt;/td&gt;
&lt;td&gt;175 / 109 / 68&lt;/td&gt;
&lt;td&gt;1,748&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2,500&lt;/td&gt;
&lt;td&gt;high protein&lt;/td&gt;
&lt;td&gt;250 / 250 / 56&lt;/td&gt;
&lt;td&gt;2,504&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every plan. Every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why rounding each one cannot work
&lt;/h2&gt;

&lt;p&gt;Because the three values are not independent. They have to satisfy&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4c + 4p + 9f = calories
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and &lt;code&gt;Math.round&lt;/code&gt; does not know that. It rounds &lt;code&gt;c&lt;/code&gt; to whatever is nearest to &lt;code&gt;c&lt;/code&gt;, which is the right answer to a question nobody asked. The constraint is on the &lt;em&gt;sum&lt;/em&gt;, and three separately-correct roundings do not add up to a correctly-rounded total.&lt;/p&gt;

&lt;p&gt;This is the same family as a percentage table where the column has to total 100% and comes out at 99.9%. The usual fix there is largest-remainder: hand out the floors, then give the spare units to whoever was robbed most by rounding down.&lt;/p&gt;

&lt;p&gt;That does not work here, and the reason is the interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The weights are 4, 4 and 9
&lt;/h2&gt;

&lt;p&gt;In a percentage table every unit is worth the same, so you can move one from anywhere to anywhere. Here, moving a gram of carbs changes the total by 4, and moving a gram of fat changes it by 9. You cannot always close a gap of 3 kcal, because nothing in your inventory is worth 3.&lt;/p&gt;

&lt;p&gt;So the order matters. Pick fat first, and pick it so that what is left over divides by 4 — because whatever is left has to be made up entirely of carbs and protein, and those only come in fours.&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;want&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;kcal&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// ideal fat, in grams&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;want&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;want&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="o"&gt;++&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;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;kcal&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;f&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;left&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&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;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// no integer c,p can finish this&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                     &lt;span class="c1"&gt;// carbs + protein, in grams&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;split&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;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rest&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the search never has to go far. &lt;code&gt;9 ≡ 1 (mod 4)&lt;/code&gt;, so incrementing &lt;code&gt;f&lt;/code&gt; by one shifts &lt;code&gt;kcal - 9f&lt;/code&gt; by one modulo 4 — which means &lt;strong&gt;one fat value in every four consecutive integers satisfies the condition&lt;/strong&gt;. A candidate always exists within two grams of the ideal. There is no failure case to handle, only a choice to make.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first version was still wrong, and quietly
&lt;/h2&gt;

&lt;p&gt;I originally took the &lt;em&gt;nearest&lt;/em&gt; valid &lt;code&gt;f&lt;/code&gt; and stopped. Every combination hit its calorie target exactly, the tests I had written passed, and I nearly shipped it.&lt;/p&gt;

&lt;p&gt;Then I measured how far the grams had drifted from the ideal split, which is the other thing the tool is promising:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;worst drift: 4.75 g on protein at 2,500 kcal keto
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keto puts 70% of the calories in fat, so fat is large, its rounding error is large in kcal, and all of that leftover lands on carbs and protein. Taking the first valid fat value meant taking whatever drift came with it.&lt;/p&gt;

&lt;p&gt;There are usually several valid candidates in that ±3 window, and they do not cost the same. Scoring all of them by total drift and keeping the best brings the worst case down to &lt;strong&gt;3.94 g&lt;/strong&gt; — under 3% of that macro — while still landing exactly on the calorie target. Seven candidates is nothing to evaluate.&lt;/p&gt;

&lt;p&gt;That is the general lesson and it is not about food:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Satisfying the constraint is necessary, not sufficient.&lt;/strong&gt; The first solution that satisfies it is rarely the best one, and if you only assert on the constraint your tests will happily bless the worst member of the solution set.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Testing it
&lt;/h2&gt;

&lt;p&gt;Sweeping, not sampling:&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="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;cals&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;cals&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &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;p&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PLANS&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;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;macroGrams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&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;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kcal&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;cals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;miss&lt;/span&gt;&lt;span class="o"&gt;++&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;516 combinations, and they all have to land exactly. Sampling round numbers would have been useless here — whether the bug shows depends on where the ideal fat value happens to fall relative to a multiple of four, and 2,000 kcal is exactly the kind of tidy input that could have gone either way.&lt;/p&gt;

&lt;p&gt;The drift bound in the test is the measured number, not a guess:&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;// 3.94 g is what the algorithm actually holds across the swept range,&lt;/span&gt;
&lt;span class="c1"&gt;// on protein in a keto split. Four is that, rounded up.&lt;/span&gt;
&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no macro drifts more than 4 g from its ideal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;worst&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;4&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writing &lt;code&gt;worst &amp;lt;= 10&lt;/code&gt; because it passes is not a test, it is a formality. Writing &lt;code&gt;worst &amp;lt;= 3&lt;/code&gt; because it sounds tidy fails on an input that is perfectly fine. Measure the bound, then assert it.&lt;/p&gt;

&lt;h2&gt;
  
  
  And say the number
&lt;/h2&gt;

&lt;p&gt;The last change was not code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Protein            149 g
Carbohydrates      198 g
Fat                 68 g
These add up to  2,000 kcal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reader was going to multiply it out anyway. Better that the page does it first.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://utilorax.com" rel="noopener noreferrer"&gt;Utilorax&lt;/a&gt;, a set of free browser-based tools. This came out of the &lt;a href="https://utilorax.com/calculators/macro-calculator" rel="noopener noreferrer"&gt;macro calculator&lt;/a&gt;, which now hands you three numbers that add up to the one you asked for.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>math</category>
      <category>webdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>A missing exchange rate is not an exchange rate of 1</title>
      <dc:creator>Hammad Shams Uddin</dc:creator>
      <pubDate>Thu, 20 Aug 2026 12:56:18 +0000</pubDate>
      <link>https://dev.to/hammad4june1999/a-missing-exchange-rate-is-not-an-exchange-rate-of-1-4474</link>
      <guid>https://dev.to/hammad4june1999/a-missing-exchange-rate-is-not-an-exchange-rate-of-1-4474</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&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;var&lt;/span&gt; &lt;span class="nx"&gt;fx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rates&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line converts a crypto price into whatever currency the visitor picked. It has been on my site for months. It is one line, it reads like defensive programming, and it is capable of being wrong by a factor of 280.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;|| 1&lt;/code&gt; means here
&lt;/h2&gt;

&lt;p&gt;The rate table comes from a currency API. When that API fails, my server-side cache falls back to the only rate it can be sure of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'data'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'USD'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is honest. &lt;code&gt;USD =&amp;gt; 1.0&lt;/code&gt; is true, and the fallback contains nothing it cannot vouch for.&lt;/p&gt;

&lt;p&gt;Then the browser asks that table for &lt;code&gt;PKR&lt;/code&gt;, gets &lt;code&gt;undefined&lt;/code&gt;, and &lt;code&gt;|| 1&lt;/code&gt; turns "I don't have that" into "the rate is 1".&lt;/p&gt;

&lt;p&gt;So the page renders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 BTC = 95,000.00 PKR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real figure is about 26,600,000. And there is no asterisk, no greyed-out state, no "approximate" — it looks exactly like the correct answer, because it is produced by exactly the same code path as the correct answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second one, in the same function
&lt;/h2&gt;

&lt;p&gt;Three lines down:&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;var&lt;/span&gt; &lt;span class="nx"&gt;ago&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cached_age_sec&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cached_age_sec&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Live spot price, updated about &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ago&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;just now&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ago&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; min ago&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cached_age_sec&lt;/code&gt; is null when the cache layer cannot say how old the figure is. The ternary is careful — it checks for null — and then maps that null onto &lt;code&gt;0&lt;/code&gt;. One line later, &lt;code&gt;0&lt;/code&gt; is read as &lt;strong&gt;"updated just now"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The one case where the freshness is genuinely unknown is the case that produces the most reassuring sentence on the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape
&lt;/h2&gt;

&lt;p&gt;Both are the same mistake wearing different clothes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A value that was absent, replaced by a plausible default, and then rendered with the same confidence as a measured one.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The defaults are not stupid. A rate of 1 is the identity. An age of 0 is the neutral element. In isolation each choice looks like the sensible thing to reach for when something is missing. The damage is done at the point of display, where the substitute and the real thing become indistinguishable.&lt;/p&gt;

&lt;p&gt;And it is worse than crashing. A crash is a bad experience that tells the truth. This tells a confident lie in the visitor's own currency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that makes it embarrassing
&lt;/h2&gt;

&lt;p&gt;The correct handling was already in the file. Twenty lines above, the plain currency converter has always done this:&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;var&lt;/span&gt; &lt;span class="nx"&gt;fr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&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="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fr&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;That currency is not in the live feed.&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;Same file. Same rate table. Same author. One engine refuses; the two next to it substitute. Nothing distinguishes them except which one I happened to write while thinking about failure.&lt;/p&gt;

&lt;p&gt;That is the real lesson and it is not about currencies: &lt;strong&gt;defensive habits do not generalise on their own.&lt;/strong&gt; Getting it right once in a file is not evidence the file is right. It is one data point about one function.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fixes
&lt;/h2&gt;

&lt;p&gt;Refuse, and name what is missing:&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;var&lt;/span&gt; &lt;span class="nx"&gt;fx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rates&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;fx&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Live rates for &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;that currency&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; are unavailable right now — try USD, or refresh in a moment.&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;Note &lt;code&gt;&amp;gt; 0&lt;/code&gt; rather than a truthiness check. A rate of &lt;code&gt;0&lt;/code&gt; is as unusable as a missing one, and &lt;code&gt;undefined &amp;gt; 0&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;, so the two collapse into the same branch without a separate test.&lt;/p&gt;

&lt;p&gt;And let unknown stay unknown:&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;function&lt;/span&gt; &lt;span class="nf"&gt;freshness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unit&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;age&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;age unknown&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;updated just now&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="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;updated about &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;unit&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; ago&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;Three sentences for three states, instead of two sentences for three states. The bug was entirely in that missing third branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing a feed that has failed
&lt;/h2&gt;

&lt;p&gt;Neither bug is reachable by using the page. The upstream API has to be &lt;em&gt;partially&lt;/em&gt; down — up enough to return a table, broken enough that your currency is not in it. I have never once seen that state in a browser, and I never will on purpose.&lt;/p&gt;

&lt;p&gt;So the engines had to become reachable from a test. They live in a browser IIFE, so they got the same export guard the widgets file already used:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ENGINES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;freshness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;trueCost&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;Then the fixture is the whole point — a feed that is deliberately, plausibly incomplete:&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;feed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;coins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bitcoin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;95000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;        &lt;span class="c1"&gt;// but not dogecoin&lt;/span&gt;
  &lt;span class="na"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;USD&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="na"&gt;EUR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.92&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;     &lt;span class="c1"&gt;// but not PKR&lt;/span&gt;
  &lt;span class="na"&gt;cached_age_sec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&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 is not a made-up shape. It is precisely what the server returns when the currency provider is down and the crypto provider is not: &lt;code&gt;['USD' =&amp;gt; 1.0]&lt;/code&gt; and nothing else.&lt;/p&gt;

&lt;p&gt;One of the nineteen assertions is about a function I did not change:&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;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ENGINES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;currencyConvert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;100&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PKR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;fx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;and a missing one was always refused&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It cannot fail today. It is there so the three engines that read the same table cannot quietly drift apart again — which is how they got out of step in the first place.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://utilorax.com" rel="noopener noreferrer"&gt;Utilorax&lt;/a&gt;, a set of free browser-based tools. This came out of the &lt;a href="https://utilorax.com/calculators/crypto-price-converter" rel="noopener noreferrer"&gt;crypto price converter&lt;/a&gt; and the &lt;a href="https://utilorax.com/calculators/gold-silver-rate-calculator" rel="noopener noreferrer"&gt;gold &amp;amp; silver price calculator&lt;/a&gt;, both of which now tell you when they don't know rather than guessing on your behalf.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>javascript</category>
      <category>api</category>
    </item>
    <item>
      <title>Your amortisation schedule is one row short, and only sometimes</title>
      <dc:creator>Hammad Shams Uddin</dc:creator>
      <pubDate>Thu, 20 Aug 2026 09:20:13 +0000</pubDate>
      <link>https://dev.to/hammad4june1999/your-amortisation-schedule-is-one-row-short-and-only-sometimes-17f9</link>
      <guid>https://dev.to/hammad4june1999/your-amortisation-schedule-is-one-row-short-and-only-sometimes-17f9</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You cannot be billed $1,896.2027.&lt;/p&gt;

&lt;p&gt;That is the whole bug, and I had it in a loan calculator and an amortisation schedule for months without noticing, because on most of the numbers I tested with it makes no difference at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The line
&lt;/h2&gt;

&lt;p&gt;Standard amortisation. Work out the monthly payment, then walk the balance down month by month:&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;let&lt;/span&gt; &lt;span class="nx"&gt;pay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;principal&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.005&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;interest&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt;   &lt;span class="nx"&gt;principalPart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pay&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;interest&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;principalPart&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;principalPart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// final payment&lt;/span&gt;
  &lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;principalPart&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;principalPart&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;interest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;interest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;balance&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 is correct arithmetic and it is not what happens to you.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pay&lt;/code&gt; comes out of that formula as something like &lt;code&gt;1896.2027149...&lt;/code&gt;. The schedule then charges &lt;code&gt;1896.2027149...&lt;/code&gt; every month for thirty years. Your lender charges you &lt;strong&gt;$1,896.20&lt;/strong&gt;, because money has two decimal places, and the two-hundredths of a cent they are not collecting has to come from somewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;$300,000 at 6.5% over 30 years, run both ways:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;payments&lt;/th&gt;
&lt;th&gt;last payment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;unrounded &lt;code&gt;1896.2027…&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;360&lt;/td&gt;
&lt;td&gt;$1,896.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rounded &lt;code&gt;1896.20&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;361&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$4.53&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The schedule said the loan ends in month 360. In reality there is a 361st bill for $4.53, because paying two-hundredths of a cent less each month leaves $4.53 of principal standing when the schedule thinks you are done.&lt;/p&gt;

&lt;p&gt;Four dollars is not the point. &lt;strong&gt;The number of rows is wrong&lt;/strong&gt;, and that is the one thing an amortisation schedule exists to tell you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it survived
&lt;/h2&gt;

&lt;p&gt;Here is the same comparison on a different loan:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;payments&lt;/th&gt;
&lt;th&gt;last payment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;unrounded&lt;/td&gt;
&lt;td&gt;360&lt;/td&gt;
&lt;td&gt;$1,380.51&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rounded&lt;/td&gt;
&lt;td&gt;360&lt;/td&gt;
&lt;td&gt;$1,379.86&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;$250,000 at 5.25%, and both methods land on 360. The rounding happens to fall the other way and the stub is absorbed by the final payment.&lt;/p&gt;

&lt;p&gt;So the bug is real, reproducible, and &lt;strong&gt;invisible on plenty of ordinary inputs&lt;/strong&gt;. Any test suite built from round numbers — $250,000, 5%, 30 years — passes with it in. Mine would have.&lt;/p&gt;

&lt;p&gt;This is a shape worth recognising, because it is not really about money:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A test case that agrees with both the correct and the incorrect implementation is not evidence of anything.&lt;/strong&gt; It feels like coverage. It is the absence of coverage, dressed up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When I wrote the tests afterwards I deliberately kept the $250,000 case in, alongside the $300,000 one. Not because it catches the bug — it cannot — but because a fix that only works on the input that exposed the problem is not a fix, and I want to know if I ever break the agreeing case too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is one line and one consequence
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;principal&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;pay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// a lender bills to the cent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consequence is that the loop has to be allowed to run past &lt;code&gt;n&lt;/code&gt;. If you wrote &lt;code&gt;for (let m = 0; m &amp;lt; n; m++)&lt;/code&gt; you now silently drop the stub payment and end with a non-zero balance, which is worse than where you started — the schedule is still 360 rows but now it does not add up. The &lt;code&gt;while (balance &amp;gt; 0.005)&lt;/code&gt; form handles it, with a guard so a pathological rate cannot spin forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same mistake, upstairs
&lt;/h2&gt;

&lt;p&gt;The summary calculator had its own version:&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;emi&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;principal&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;emi&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                    &lt;span class="c1"&gt;// &amp;lt;- both wrong things at once&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;total&lt;/code&gt; multiplies the &lt;strong&gt;unrounded&lt;/strong&gt; payment, while the page prints the &lt;strong&gt;rounded&lt;/strong&gt; one. So "Monthly payment $1,896.20" and "Total of payments $682,633.47" are not consistent with each other: a visitor with a phone can multiply 1896.20 by 360, get $682,632.00, and be right that something is off.&lt;/p&gt;

&lt;p&gt;And it assumes the term is exactly &lt;code&gt;n&lt;/code&gt; payments, which is the bug above wearing a different hat.&lt;/p&gt;

&lt;p&gt;Running the real schedule fixes both, and 360 iterations of arithmetic costs nothing:&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;function&lt;/span&gt; &lt;span class="nf"&gt;trueCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;monthlyRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;months&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;principal&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;monthlyRate&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;monthlyRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;months&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;pay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;paid&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="nx"&gt;interest&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="nx"&gt;count&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;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.005&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;months&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="o"&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;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;monthlyRate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;principalPart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pay&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;i&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;principalPart&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;principalPart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;principalPart&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;balance&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.005&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;balance&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="nx"&gt;interest&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;paid&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;principalPart&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&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="na"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;months&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;paid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;interest&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;It also gives you the number of payments for free, which is worth showing — it is the answer to "when am I actually finished", and it is not always the number you typed in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests a borrower could run
&lt;/h2&gt;

&lt;p&gt;The assertions that turned out to be worth the most are the ones checkable against a real statement, not against another implementation of the same formula:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every dollar of principal is repaid — no more, no less&lt;/li&gt;
&lt;li&gt;interest plus principal equals what was paid&lt;/li&gt;
&lt;li&gt;the final balance is exactly zero, not &lt;code&gt;1e-9&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the payment is a whole number of cents&lt;/li&gt;
&lt;li&gt;an extra $200 a month shortens the term &lt;em&gt;and&lt;/em&gt; costs less interest&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those restate the amortisation formula, which is the point. A test that recomputes &lt;code&gt;principal * r / (1 - (1+r)^-n)&lt;/code&gt; and compares it to the code passes whether or not the code is right about anything else.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://utilorax.com" rel="noopener noreferrer"&gt;Utilorax&lt;/a&gt;, a set of free browser-based tools. This came out of the &lt;a href="https://utilorax.com/calculators/loan-calculator" rel="noopener noreferrer"&gt;loan calculator&lt;/a&gt;, the &lt;a href="https://utilorax.com/calculators/mortgage-calculator" rel="noopener noreferrer"&gt;mortgage calculator&lt;/a&gt; and the &lt;a href="https://utilorax.com/calculators/loan-amortization-schedule" rel="noopener noreferrer"&gt;amortisation schedule&lt;/a&gt;, all three of which now count the 361st payment.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>javascript</category>
      <category>testing</category>
    </item>
    <item>
      <title>I made twenty tools take twenty files. Every bug was state that outlived one file.</title>
      <dc:creator>Hammad Shams Uddin</dc:creator>
      <pubDate>Wed, 19 Aug 2026 16:56:10 +0000</pubDate>
      <link>https://dev.to/hammad4june1999/i-made-twenty-tools-take-twenty-files-every-bug-was-state-that-outlived-one-file-4898</link>
      <guid>https://dev.to/hammad4june1999/i-made-twenty-tools-take-twenty-files-every-bug-was-state-that-outlived-one-file-4898</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My browser-based tools each took exactly one file. Converting twenty tracks meant twenty page loads, and on the background remover each of those re-downloaded a 44 MB segmentation model to do three seconds of work.&lt;/p&gt;

&lt;p&gt;So I put a loop around the work. That is genuinely most of it — read the settings once, run them over each file, collect the results, zip them.&lt;/p&gt;

&lt;p&gt;Then I tested it in a real browser and found three bugs. They look unrelated. They are the same bug: &lt;strong&gt;something that was fine as a single value became wrong the moment there were two files, and none of them announced itself.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug one: the second file overwrote the first
&lt;/h2&gt;

&lt;p&gt;The single-file version wrote into a fixed name on ffmpeg.wasm's virtual filesystem:&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;function&lt;/span&gt; &lt;span class="nf"&gt;runSingle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&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;inN&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;name&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;outN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;out.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&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 filesystem is shared for the whole session, not per call. Loop it, and clip two writes &lt;code&gt;in.mp4&lt;/code&gt; over clip one — which, if you are unlucky with timing, is not even a crash. It is a correct-looking output that contains the wrong video.&lt;/p&gt;

&lt;p&gt;The fix is boring: the index goes in the name.&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;function&lt;/span&gt; &lt;span class="nf"&gt;runSingle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idx&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;inN&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;outN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;out&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What is &lt;em&gt;not&lt;/em&gt; boring is the second half. A file that fails leaves its input behind, and the next twelve files then run in a workspace that is filling up with dead inputs. So the cleanup has to happen on the failure path too, not just the success one:&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="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outN&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;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;- the one people forget&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&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;h2&gt;
  
  
  Bug two: the array that was empty for the whole run
&lt;/h2&gt;

&lt;p&gt;This is the one worth the article.&lt;/p&gt;

&lt;p&gt;Two output files can want the same name. &lt;code&gt;song.wav&lt;/code&gt; and &lt;code&gt;song.flac&lt;/code&gt; both convert to &lt;code&gt;song.mp3&lt;/code&gt;, and a zip quietly keeps only the last of a repeated name — so one of the two conversions silently vanishes. I wrote a dedupe for it:&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;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uniqueName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stem&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// compare against what we have so far&lt;/span&gt;
  &lt;span class="na"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a preview, so a batch shows a picture rather than only a list of names:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;showPreview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// only the first one&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both read &lt;code&gt;outputs&lt;/code&gt;. And &lt;code&gt;outputs&lt;/code&gt; is assigned like this:&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;series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// &amp;lt;- after every file has finished&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputs&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;So for the entire duration of the run — the only time those two lines execute — &lt;code&gt;outputs&lt;/code&gt; is &lt;code&gt;[]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The consequences are not symmetrical, which is why this took a test to catch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;!outputs.length&lt;/code&gt; was &lt;strong&gt;always true&lt;/strong&gt;, so every file overwrote the preview. The batch appeared to work and showed the last image. Nothing looked broken; the preview was just the wrong file, and only if you knew which one you dropped first would you notice.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;uniqueName(want, [])&lt;/code&gt; &lt;strong&gt;never found a clash&lt;/strong&gt;, so &lt;code&gt;cat.png&lt;/code&gt; and &lt;code&gt;cat.jpg&lt;/code&gt; both produced &lt;code&gt;cat-no-bg.png&lt;/code&gt; and the zip contained one of them. The results list on screen showed the name twice, which is the only reason I looked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is one line and it is not the dedupe:&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;made&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;                       &lt;span class="c1"&gt;// accumulate where the work happens&lt;/span&gt;
&lt;span class="nf"&gt;series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&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;o&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uniqueName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;made&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;out&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;made&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;showPreview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;made&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&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;o&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;done&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 general shape:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A variable assigned from a promise's result is empty for exactly as long as the work that produces it is running.&lt;/strong&gt; If anything inside that work reads it, it reads the empty version, every time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is easy to miss because the code reads correctly in the order it is written. &lt;code&gt;outputs = res.done&lt;/code&gt; sits above &lt;code&gt;render(outputs)&lt;/code&gt; and below the loop, so it looks like it happens between them. It happens after both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug three: an operation completing is not evidence it did anything
&lt;/h2&gt;

&lt;p&gt;I have written about this one &lt;a href="https://dev.to/hammad4june1999/the-file-existed-so-i-assumed-the-check-passed-46h5"&gt;before&lt;/a&gt;, and batching brings it back sharper. &lt;code&gt;ffmpeg.run()&lt;/code&gt; resolves whether ffmpeg succeeded or not. With one file, a silent failure means the visitor sees an error a moment later and tries again. With twenty, one silent failure means &lt;strong&gt;nineteen good files and one wrong one, in a zip, unremarked.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So each iteration has to judge the output, not the promise:&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;readFile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outN&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ffmpeg wrote no output for &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;And the failure has to be per-file rather than fatal. One unreadable file out of twenty must not cost someone the other nineteen — it should be named on the results page, next to the files that worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alpha.mp3          7 KB   Save
song.mp3           6 KB   Save
song (2).mp3       5 KB   Save
1 file could not be processed and is not included. broken.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sequential, not Promise.all
&lt;/h2&gt;

&lt;p&gt;Worth saying because it looks like the obvious optimisation. There is one ffmpeg instance behind one core and one ONNX session. Running the files concurrently does not make them finish sooner; it interleaves them, multiplies peak memory by the number of files, and turns a readable progress bar into a lie.&lt;/p&gt;

&lt;p&gt;The batch is a &lt;code&gt;reduce&lt;/code&gt; over promises, and the bar gives each file an equal slice:&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;function&lt;/span&gt; &lt;span class="nf"&gt;setProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&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;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;clamp01&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;%&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;h2&gt;
  
  
  How I actually found these
&lt;/h2&gt;

&lt;p&gt;None of the three would have been caught by reading the diff, and two of them produce a page that looks like it worked.&lt;/p&gt;

&lt;p&gt;They came out of driving the shipped page in a real browser: build the fixtures in JavaScript, hand them to the widget as a synthetic &lt;code&gt;DataTransfer&lt;/code&gt;, click the button, and read the DOM afterwards. The fixtures are the part that matters — four files where two deliberately fight over one output name and one is eight bytes of garbage:&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="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alpha.wav&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;wav&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;440&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;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;song.wav&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="nf"&gt;wav&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&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;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;song.m4a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="nf"&gt;wav&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;   &lt;span class="c1"&gt;// same stem -&amp;gt; collision&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;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;broken.wav&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A happy-path batch of three well-named valid files passes with all three bugs present. That is not a test; it is a screenshot with extra steps.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://utilorax.com" rel="noopener noreferrer"&gt;Utilorax&lt;/a&gt;, a set of free browser-based tools. The batch now runs on the &lt;a href="https://utilorax.com/documents/audio-converter" rel="noopener noreferrer"&gt;audio converter&lt;/a&gt;, the &lt;a href="https://utilorax.com/documents/video-convert" rel="noopener noreferrer"&gt;video converter&lt;/a&gt; and the &lt;a href="https://utilorax.com/documents/background-remover" rel="noopener noreferrer"&gt;background remover&lt;/a&gt; — nothing is uploaded, so the loop and the failures are all on your own machine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>javascript</category>
      <category>ffmpeg</category>
    </item>
    <item>
      <title>The file existed, so I assumed the check passed</title>
      <dc:creator>Hammad Shams Uddin</dc:creator>
      <pubDate>Fri, 14 Aug 2026 22:44:18 +0000</pubDate>
      <link>https://dev.to/hammad4june1999/the-file-existed-so-i-assumed-the-check-passed-46h5</link>
      <guid>https://dev.to/hammad4june1999/the-file-existed-so-i-assumed-the-check-passed-46h5</guid>
      <description>&lt;p&gt;A user sent me two clips and a screenshot. My video merge tool had run to 100%, said "Applying transitions…", and then thrown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FS error: readFile 'merged.mp4'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error is honest and useless. &lt;code&gt;merged.mp4&lt;/code&gt; does not exist. It does not say why, because by the time you ask, the thing that should have made it has already finished and moved on.&lt;/p&gt;

&lt;p&gt;The cause turned out to be four functions upstream, and the shape of the mistake is one I keep meeting: &lt;strong&gt;a check that tests whether something exists when the question was whether it has a particular property.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tool does
&lt;/h2&gt;

&lt;p&gt;Merging clips with different sizes and formats means normalising them first — same dimensions, same frame rate, same codecs — and only then joining. If the user asked for a transition, the join is an &lt;code&gt;xfade&lt;/code&gt; for the picture and an &lt;code&gt;acrossfade&lt;/code&gt; for the sound.&lt;/p&gt;

&lt;p&gt;That second one matters more than it looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check
&lt;/h2&gt;

&lt;p&gt;Normalising ran this per clip:&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="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-vf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-c:v&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;libx264&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-preset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;veryfast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-crf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;23&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-c:a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;libmp3lame&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-b:a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;128k&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&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="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;readFile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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 audio in this clip — redo it with a silent track&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ...anullsrc version... */&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;Read the intent: &lt;em&gt;if the encode failed because the source had no audio, redo it with generated silence, so every input to the transition has a sound track.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Read what it does: &lt;em&gt;if the output file is missing, redo it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Those are not the same sentence, and the gap between them is the bug.&lt;/p&gt;

&lt;p&gt;Give &lt;code&gt;ffmpeg&lt;/code&gt; a video-only file and ask it to encode audio with &lt;code&gt;-c:a libmp3lame&lt;/code&gt;, and it does not fail. There is no audio stream, so it encodes the video, writes no audio stream, and exits successfully. &lt;strong&gt;The file exists.&lt;/strong&gt; The &lt;code&gt;catch&lt;/code&gt; never runs. The silence fallback — written specifically for this case — never fires.&lt;/p&gt;

&lt;p&gt;Then the transition builds its filter graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;0:a][1:a]acrossfade&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.5[aout]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;[0:a]&lt;/code&gt; does not exist. The graph fails, no output is written, and one &lt;code&gt;.then()&lt;/code&gt; later you get an error about &lt;code&gt;merged.mp4&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it survived
&lt;/h2&gt;

&lt;p&gt;Two reasons, and the second is the interesting one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clips with sound worked perfectly.&lt;/strong&gt; Every file I had tested with was a screen recording with audio or a phone video. The failing case is a clip with no audio track at all — which describes every AI-generated clip, most screen recordings, and any video someone has already stripped the sound from. It is not an edge case. It is a whole category of ordinary file I did not happen to own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And ffmpeg.wasm does not reject on ffmpeg failure.&lt;/strong&gt; &lt;code&gt;ffmpeg.run()&lt;/code&gt; resolves whether the command succeeded or not. A filter graph that fails produces no output and no rejection, so the &lt;code&gt;await&lt;/code&gt; returns normally and execution carries on into code that assumes the file is there. Every error in this class arrives late and blames the wrong line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is to ask the actual question
&lt;/h2&gt;

&lt;p&gt;You cannot infer "has an audio stream" from "a file appeared". So ask:&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;function&lt;/span&gt; &lt;span class="nf"&gt;hasAudio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&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;probe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;probe_&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.mka&lt;/span&gt;&lt;span class="dl"&gt;'&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;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-vn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0:a:0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-c&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;copy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-t&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.05&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&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;let&lt;/span&gt; &lt;span class="nx"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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="nx"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;readFile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;probe&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;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&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="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unlink&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&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;found&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;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&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;&lt;code&gt;-map 0:a:0&lt;/code&gt; names the first audio stream. If there is no audio stream, the mapping cannot be satisfied and nothing is written. It is a stream copy of 50 milliseconds, so it costs nothing beside the encode that follows, and the answer is about the thing I actually care about.&lt;/p&gt;

&lt;p&gt;Then the command is chosen once, rather than attempted and repaired:&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;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;audible&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-vf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-c:a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;libmp3lame&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outN&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-f&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lavfi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anullsrc=channel_layout=stereo:sample_rate=44100&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-shortest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0:v&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1:a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-vf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="nx"&gt;outN&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I did consider reading ffmpeg's log output for &lt;code&gt;Stream #0:1: Audio:&lt;/code&gt; instead. It works, and I did not use it, because 0.11 and the 0.12 adapter surface logging differently and I would have had two code paths to keep true. A probe behaves identically on both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing something that only runs in a browser
&lt;/h2&gt;

&lt;p&gt;There is no ffmpeg binary on my machine or my server — the whole point is that this runs in the user's browser via WebAssembly. And headless Chrome's virtual time does not advance a web worker, so the usual scripted approach hangs at the first &lt;code&gt;run()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What worked: a page that builds its own fixtures with &lt;code&gt;lavfi&lt;/code&gt;, runs the real checks, and POSTs its results back to a PHP file so the run can outlive any screenshot timing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;made sil.mp4=28362B snd.mp4=48141B
ok   snd.mp4 -&amp;gt; hasAudio=true    (expected true)
ok   sil.mp4 -&amp;gt; hasAudio=false   (expected false)
ok   acrossfade on silent clips produces no output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That third line is the one worth writing. It is not a test of the fix — it is a test of the &lt;strong&gt;diagnosis&lt;/strong&gt;. Before trusting the repair, prove the thing you blamed actually misbehaves the way you claim. I have shipped fixes for problems I had misdiagnosed, and they are worse than the original bug, because now the symptom is gone and the cause is still there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general version
&lt;/h2&gt;

&lt;p&gt;The bug is not really about ffmpeg. It is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;An operation completing is not evidence that it did what you wanted.&lt;/strong&gt; It is evidence that it did not crash.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Some places the same shape shows up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a file exists, so the write must have contained data&lt;/li&gt;
&lt;li&gt;an HTTP request returned 200, so the response must be the resource you asked for&lt;/li&gt;
&lt;li&gt;a command exited 0, so it must have produced output&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;catch&lt;/code&gt; block did not run, so the happy path must have happened&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of those is a check on the wrong noun. The fix is always the same and always feels like extra work: name the property you actually depend on, and ask about &lt;em&gt;that&lt;/em&gt; — not about whatever is easiest to observe nearby.&lt;/p&gt;

&lt;p&gt;And add the belt-and-braces, because you will get it wrong again somewhere. If the transition now fails for a reason I have not thought of, the tool joins the clips without one and says so, rather than handing anyone &lt;code&gt;FS error: readFile 'merged.mp4'&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://utilorax.com" rel="noopener noreferrer"&gt;Utilorax&lt;/a&gt;, a set of free browser-based tools — this one came out of the &lt;a href="https://utilorax.com/documents/video-merge" rel="noopener noreferrer"&gt;video merger&lt;/a&gt;, which does now handle a silent clip.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>ffmpeg</category>
      <category>webdev</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Two scrolling panes is the wrong way to build a side-by-side diff</title>
      <dc:creator>Hammad Shams Uddin</dc:creator>
      <pubDate>Wed, 12 Aug 2026 20:02:50 +0000</pubDate>
      <link>https://dev.to/hammad4june1999/two-scrolling-panes-is-the-wrong-way-to-build-a-side-by-side-diff-3ehn</link>
      <guid>https://dev.to/hammad4june1999/two-scrolling-panes-is-the-wrong-way-to-build-a-side-by-side-diff-3ehn</guid>
      <description>&lt;p&gt;I had a text-compare tool that asked people to compare two things it would not show them at the same time.&lt;/p&gt;

&lt;p&gt;One textarea, then another textarea underneath it, then a single-column unified diff below that — removals in red, additions in green, in the order a diff algorithm emits them. Everything on the page was correct and the tool was close to useless, because comparing is a thing your eyes do in parallel and the layout made it serial.&lt;/p&gt;

&lt;p&gt;Rebuilding it as two columns turned out to have three problems in it. Only one of them is layout, and the interesting one is the layout problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the diff you already have
&lt;/h2&gt;

&lt;p&gt;A line-level diff comes out of a longest-common-subsequence backtrack as a flat list:&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="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;same&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The quick brown fox&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;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;del&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jumps over the lazy dog&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;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;add&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="na"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;leaps over the lazy dog&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;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;same&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;and runs away.&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 is exactly what a unified view wants. It is not what a side-by-side view wants, and the difference is the whole job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem one: an edited line is two entries
&lt;/h2&gt;

&lt;p&gt;In that list, editing a line produces a &lt;code&gt;del&lt;/code&gt; and an &lt;code&gt;add&lt;/code&gt; — and in a longer diff they can be several rows apart, because the algorithm emits a run of removals and then a run of additions.&lt;/p&gt;

&lt;p&gt;Render that naively into two columns and you get the removal on row 4 of the left column and the addition on row 9 of the right one. The view is now &lt;em&gt;worse&lt;/em&gt; than the unified one: two columns, and the thing you are comparing is in different places in each.&lt;/p&gt;

&lt;p&gt;So the walk has to buffer. Consecutive non-&lt;code&gt;same&lt;/code&gt; entries are collected as a block, then paired off index by index:&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;function&lt;/span&gt; &lt;span class="nf"&gt;sideBySide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&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;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;na&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="nx"&gt;nb&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="c1"&gt;// line numbers, counted per side&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;diff&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;same&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="nx"&gt;na&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;nb&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;same&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;na&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;continue&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;dels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;adds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;diff&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;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;same&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;del&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;dels&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adds&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;k&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="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;adds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="o"&gt;++&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;da&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;dels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;dels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;adds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;adds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;da&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;na&lt;/span&gt;&lt;span class="o"&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;db&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;nb&lt;/span&gt;&lt;span class="o"&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;da&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;na&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;da&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;else&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;da&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;del&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;na&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;da&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;                            &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;add&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;na&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;db&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;return&lt;/span&gt; &lt;span class="nx"&gt;rows&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;Two things worth noticing.&lt;/p&gt;

&lt;p&gt;The blocks are rarely the same length. Three lines removed and one added means one paired row and two rows with an empty right side. That is correct — the surplus really has no counterpart — and it is why the pairing is a &lt;code&gt;max()&lt;/code&gt; loop rather than a &lt;code&gt;zip&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And the line numbers count &lt;strong&gt;separately per side&lt;/strong&gt;, which is what every code review tool does and what people expect without being able to say so. After an insertion the two columns are permanently offset, and a number that pointed at "row 7 of the display" instead of "line 7 of that version" would be useless the moment you tried to find the line in your editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem two: the eye still has to re-read both lines
&lt;/h2&gt;

&lt;p&gt;Once &lt;code&gt;jumps over the lazy dog&lt;/code&gt; and &lt;code&gt;leaps over the lazy dog&lt;/code&gt; sit next to each other, you still have to read both to find what moved. On a long line you read it three times.&lt;/p&gt;

&lt;p&gt;The fix is the same algorithm one level down. Run the line diff over the two lines' &lt;em&gt;word&lt;/em&gt; arrays and mark the tokens that differ:&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;function&lt;/span&gt; &lt;span class="nf"&gt;wordSegments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;ta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;tb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// captured group keeps the spacing&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nf"&gt;lineDiff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&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;blank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;same&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="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&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;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;del&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="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;blank&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;                     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;blank&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;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&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;Splitting on a &lt;strong&gt;captured&lt;/strong&gt; separator (&lt;code&gt;/(\s+)/&lt;/code&gt; rather than &lt;code&gt;/\s+/&lt;/code&gt;) is what makes this work. The whitespace comes back as its own token, so it survives the diff and the rebuilt line still reads like the original. Without it, marks land in the right places and every run of spaces collapses to one.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;blank&lt;/code&gt; check earns its place too. Whitespace tokens rarely match exactly — one line has two spaces where the other has three — so without it you get coloured blobs floating between words, drawing the eye to nothing.&lt;/p&gt;

&lt;p&gt;One guard: &lt;code&gt;lineDiff&lt;/code&gt; builds an &lt;code&gt;(n+1) × (m+1)&lt;/code&gt; table, so two very long lines are quadratic in a way nobody wants on a keystroke. Past a threshold, mark the whole line and move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem three: two scrolling panes is a trap
&lt;/h2&gt;

&lt;p&gt;This is the one I would have got wrong if I had started from the picture instead of the behaviour.&lt;/p&gt;

&lt;p&gt;The obvious implementation of a side-by-side view is two panes, each scrolling its own content. It looks right in a mockup. It is a bad idea, and the reason is not performance or complexity — it is that &lt;strong&gt;the panes can drift&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two independently scrollable panes have two scroll positions. Keeping them in step means listening to &lt;code&gt;scroll&lt;/code&gt; on both, writing to the other, and guarding against the feedback loop that creates. Then you discover the panes have different content heights — one side has extra rows where the other has gaps — so equal &lt;code&gt;scrollTop&lt;/code&gt; does not mean equal &lt;em&gt;position&lt;/em&gt;, and you are mapping between them.&lt;/p&gt;

&lt;p&gt;And the moment they are out of step by even one row, the view stops doing the one thing it exists to do. A diff that shows you line 40 on the left and line 41 on the right is not a diff; it is two lists.&lt;/p&gt;

&lt;p&gt;The alternative is boring and cannot drift:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"diff-row"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"diff-cell"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;…left…&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"diff-cell"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;…right…&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.diff-split-body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;max-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;32rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c"&gt;/* one scroller */&lt;/span&gt;
&lt;span class="nc"&gt;.diff-row&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.diff-cell&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;.6rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&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;One scroll container, one grid, two cells per row. The two halves of a row are in the same row element, so there is no state that could disagree. No scroll listeners, no synchronisation, no drift — because there is nothing to synchronise.&lt;/p&gt;

&lt;p&gt;The general version of this: &lt;strong&gt;when two things must stay aligned, prefer a structure where misalignment is unrepresentable over code that keeps them aligned.&lt;/strong&gt; Synchronising code has a bug rate. A shared row does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bit that only shows up on a phone
&lt;/h2&gt;

&lt;p&gt;Two columns of prose at 380px is unreadable, so under 720px both the inputs and the diff collapse to one column — old line above new, with a coloured left edge to say which is which.&lt;/p&gt;

&lt;p&gt;The tempting shortcut is to keep the column headers and hide one. I did that first. On a stacked list the surviving header sits above rows from &lt;em&gt;both&lt;/em&gt; versions and confidently mislabels half of them. Dropping the header entirely is more honest: the red and green edges already carry the information, and no text is better than text that is wrong for every second row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worth testing, and easy to
&lt;/h2&gt;

&lt;p&gt;The pairing walk is pure data in, data out, which makes it worth pinning down properly. The cases that matter are not the happy path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an edit lands on &lt;strong&gt;one&lt;/strong&gt; row, old left and new right&lt;/li&gt;
&lt;li&gt;a pure insertion has nothing on the left, and A's numbering does not advance&lt;/li&gt;
&lt;li&gt;a pure deletion is the same in reverse&lt;/li&gt;
&lt;li&gt;an uneven block (three removed, one added) pairs what it can and gives the surplus its own rows&lt;/li&gt;
&lt;li&gt;identical input produces only &lt;code&gt;same&lt;/code&gt; rows&lt;/li&gt;
&lt;li&gt;an empty diff produces no rows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus, for the word marks: only the changed word gets marked, both lines rebuild to exactly the original text, whitespace is never marked, and the long-line guard does not lose any text.&lt;/p&gt;

&lt;p&gt;Twenty-three assertions, no DOM, runs in a second. The line-numbering-after-an-insert case caught a real bug — worth more than the rest of the suite put together, and the one I would not have thought to check by hand.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://utilorax.com" rel="noopener noreferrer"&gt;Utilorax&lt;/a&gt;, a set of free browser-based tools — including the &lt;a href="https://utilorax.com/tools/text-compare" rel="noopener noreferrer"&gt;text compare&lt;/a&gt; this came out of.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>algorithms</category>
      <category>ux</category>
    </item>
    <item>
      <title>Working out what a site is running — and where every check lies to you</title>
      <dc:creator>Hammad Shams Uddin</dc:creator>
      <pubDate>Tue, 11 Aug 2026 17:09:51 +0000</pubDate>
      <link>https://dev.to/hammad4june1999/working-out-what-a-site-is-running-and-where-every-check-lies-to-you-167p</link>
      <guid>https://dev.to/hammad4june1999/working-out-what-a-site-is-running-and-where-every-check-lies-to-you-167p</guid>
      <description>&lt;p&gt;You want to know what some site is built on. Maybe a competitor, maybe a client's old site nobody has the credentials for, maybe you just saw something fast and want to know why.&lt;/p&gt;

&lt;p&gt;There are four places to look, and each one will confidently tell you something false under conditions you should know about. The checks are easy. Knowing when to disbelieve them is the actual skill.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. DNS, and the CDN that hides everything behind it
&lt;/h2&gt;

&lt;p&gt;The first instinct is to resolve the domain and see where it lands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is genuinely useful and it is also the check that misleads people most often, because an IP in front of a CDN is the CDN, not the host.&lt;/p&gt;

&lt;p&gt;I ran this on my own site recently. The IP that came back belongs to Cloudflare. My actual server is on a shared host in a different country entirely, and nothing in DNS will tell you that — which is most of the point of putting Cloudflare there.&lt;/p&gt;

&lt;p&gt;So the rule is: &lt;strong&gt;if the IP resolves to Cloudflare, Fastly, Akamai or a big cloud edge, you have learned who fronts the site and nothing about who hosts it.&lt;/strong&gt; That is still worth knowing. Just do not write "hosted on Cloudflare" in your notes.&lt;/p&gt;

&lt;p&gt;The records that survive the CDN are the ones it does not proxy. &lt;code&gt;MX&lt;/code&gt; almost always points at the real mail provider. &lt;code&gt;TXT&lt;/code&gt; is a gift — SPF records list every service allowed to send mail as that domain, which in practice is a list of the SaaS products the company pays for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short example.com MX
dig +short example.com TXT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My own domain demonstrates the whole point in two lines. The A record says Cloudflare (&lt;code&gt;AS13335&lt;/code&gt;). The MX record says &lt;code&gt;mx1.hostinger.com&lt;/code&gt;. The first tells you nothing about where the site lives; the second quietly names the host, because mail does not route through the CDN.&lt;/p&gt;

&lt;p&gt;If you are not at a terminal, a browser &lt;a href="https://utilorax.com/tools/dns-lookup" rel="noopener noreferrer"&gt;DNS lookup&lt;/a&gt; will pull the same records, and there is a &lt;a href="https://utilorax.com/tools/domain-to-ip" rel="noopener noreferrer"&gt;domain to IP&lt;/a&gt; tool for the plain A-record case.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Response headers, which are honest by accident
&lt;/h2&gt;

&lt;p&gt;Headers are the highest-signal-per-second check there is, because they are mostly emitted by machines that have no interest in your investigation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What to read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;server&lt;/code&gt; — sometimes the real thing (&lt;code&gt;nginx&lt;/code&gt;, &lt;code&gt;Apache&lt;/code&gt;), often replaced by the CDN.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;x-powered-by&lt;/code&gt; — when present, usually accurate and usually an oversight.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cf-cache-status&lt;/code&gt; — Cloudflare, and more interesting than it looks. &lt;code&gt;HIT&lt;/code&gt; means the edge served it. &lt;code&gt;DYNAMIC&lt;/code&gt; means Cloudflare was told not to cache this. &lt;code&gt;BYPASS&lt;/code&gt; means something upstream explicitly said no.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;via&lt;/code&gt;, &lt;code&gt;x-vercel-id&lt;/code&gt;, &lt;code&gt;x-amz-cf-id&lt;/code&gt; — the infrastructure signing its own work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That &lt;code&gt;cf-cache-status&lt;/code&gt; line is worth dwelling on, because it varies by path on the same site and the variation is the configuration, visible from outside. Two requests against my own domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="err"&gt;/&lt;/span&gt;public/css/style.min.css   &lt;span class="ss"&gt;HIT&lt;/span&gt;       cache-control: public, max-age=31536000, immutable
&lt;span class="err"&gt;/&lt;/span&gt;tools/dns-lookup           DYNAMIC   cache-control: private, max-age=0, must-revalidate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assets cached at the edge for a year, HTML never cached at all. That is a deliberate setup and you can read it off in about four seconds without any access to the site.&lt;/p&gt;

&lt;p&gt;The lie to watch for: headers describe &lt;strong&gt;the machine that answered&lt;/strong&gt;, which on a CDN is an edge node in your own city, not the origin. A missing &lt;code&gt;x-powered-by&lt;/code&gt; means the CDN stripped it, not that the site is not running PHP.&lt;/p&gt;

&lt;p&gt;If you want status codes and the full redirect chain rather than raw headers, &lt;a href="https://utilorax.com/tools/http-status-checker" rel="noopener noreferrer"&gt;this status code checker&lt;/a&gt; follows the chain and shows each hop, which is the part that matters when you are chasing a redirect loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The HTML, which is where the CMS gives itself away
&lt;/h2&gt;

&lt;p&gt;Frameworks leave litter. Fetch the page and look for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;meta name="generator"[^&amp;gt;]*&amp;gt;'&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s1"&gt;'/wp-(content|includes)/[^"]*'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/wp-content/themes/something/&lt;/code&gt; in an asset URL names the WordPress theme outright. &lt;code&gt;&amp;lt;meta name="generator" content="WordPress 6.x"&amp;gt;&lt;/code&gt; does the same more politely. Shopify, Ghost, Wix and Squarespace all announce themselves similarly if nobody has bothered to turn it off.&lt;/p&gt;

&lt;p&gt;There is a &lt;a href="https://utilorax.com/tools/wordpress-theme-detector" rel="noopener noreferrer"&gt;WordPress theme detector&lt;/a&gt; that does this pattern-matching for you and also reports the plugins it can see from asset paths, which is often the more interesting half.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lie here is the biggest one on the list, and it is not the HTML's fault — it is yours.&lt;/strong&gt; Plenty of pages build their content in JavaScript, so what &lt;code&gt;curl&lt;/code&gt; returns is a shell and what a browser shows is the page. If you grep the raw HTML and find nothing, you have not learned that there is nothing there.&lt;/p&gt;

&lt;p&gt;I lost an evening to exactly this last week. I was checking whether a publication's guest-author page linked to an application form, grepped the HTML for &lt;code&gt;mailto:&lt;/code&gt; and forms, found neither, and concluded the route was elsewhere. The page renders its body client-side. Everything I wanted was there, in the browser, the whole time.&lt;/p&gt;

&lt;p&gt;When the raw HTML looks empty, that is the signal to open devtools and read the rendered DOM instead — not to conclude anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. robots.txt, which tells you what exists
&lt;/h2&gt;

&lt;p&gt;This one is under-used and it costs one request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/robots.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Disallow&lt;/code&gt; lines are a list of things the site owner knows about and would rather you did not crawl. That is a site map of sorts, drawn by someone with full knowledge.&lt;/p&gt;

&lt;p&gt;A concrete example. I was evaluating a service to see whether their listings link back to the products they list. Their &lt;code&gt;robots.txt&lt;/code&gt; disallowed &lt;code&gt;/provider/&lt;/code&gt; and &lt;code&gt;/developer/&lt;/code&gt; — which told me those sections existed, that they were considered low-value, and that whatever lives there is not meant to be indexed. Combined with the listing page carrying no outbound link at all, the picture was complete in about four minutes.&lt;/p&gt;

&lt;p&gt;The lie: &lt;code&gt;robots.txt&lt;/code&gt; is a request, not a wall. Paths listed there are frequently still reachable and sometimes still indexed. It tells you about intent, not access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it in order
&lt;/h2&gt;

&lt;p&gt;For a site you know nothing about, this is about five minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;curl -sI&lt;/code&gt; — status, server, CDN, redirect chain&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;curl -s .../robots.txt&lt;/code&gt; — what exists, what they hide&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dig MX&lt;/code&gt; and &lt;code&gt;dig TXT&lt;/code&gt; — real mail host, and every SaaS they pay for&lt;/li&gt;
&lt;li&gt;Page source for &lt;code&gt;generator&lt;/code&gt; and &lt;code&gt;/wp-content/&lt;/code&gt; — the CMS&lt;/li&gt;
&lt;li&gt;If the source is thin, open devtools and read the rendered DOM&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the corresponding list of what each one cannot tell you, which is the part worth memorising:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Confidently wrong about&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A record&lt;/td&gt;
&lt;td&gt;the origin, whenever a CDN is in front&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Response headers&lt;/td&gt;
&lt;td&gt;the origin, again — it is the edge answering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw HTML&lt;/td&gt;
&lt;td&gt;anything rendered client-side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;robots.txt&lt;/td&gt;
&lt;td&gt;what is actually reachable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these are exotic failure modes. They are the normal case on any site built in the last five years, which is why the checks are worth running together rather than one at a time. Each one covers the specific blind spot of another.&lt;/p&gt;

&lt;p&gt;The version of this I keep having to relearn: a check that returns cleanly has not necessarily answered your question. It has answered &lt;em&gt;its&lt;/em&gt; question, and it is on you to know whether that was the one you asked.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://utilorax.com" rel="noopener noreferrer"&gt;Utilorax&lt;/a&gt;, a set of free browser-based tools — the ones linked above among them. They exist because I got tired of not having a terminal handy on someone else's laptop.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>tooling</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I found 216 missing translations. There were 55.</title>
      <dc:creator>Hammad Shams Uddin</dc:creator>
      <pubDate>Tue, 11 Aug 2026 16:26:19 +0000</pubDate>
      <link>https://dev.to/hammad4june1999/i-found-216-missing-translations-there-were-55-1lgh</link>
      <guid>https://dev.to/hammad4june1999/i-found-216-missing-translations-there-were-55-1lgh</guid>
      <description>&lt;p&gt;I run a site with about 6,000 pages across five languages. Last week I went looking for interface strings that had never been translated — the labels on form fields, the options in a dropdown, the text on a button.&lt;/p&gt;

&lt;p&gt;I wrote a script. It walked the tool registry, collected every &lt;code&gt;input_label&lt;/code&gt;, every option label, every placeholder, every select choice, and diffed that set against the Spanish translation catalogue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;216 missing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I spent the evening writing translations for all of them, in four languages.&lt;/p&gt;

&lt;p&gt;They were not missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two dictionaries
&lt;/h2&gt;

&lt;p&gt;The site has two translation catalogues, and I had forgotten there were two.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data/i18n/{lang}/tool-labels.php     read by the PHP view
data/i18n/{lang}/widget-labels.php   shipped to the browser as JSON
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first one is for strings the server renders into the HTML — form labels, buttons, the options inside a &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;. The second is for strings the JavaScript builds at runtime, after the page has loaded: the word "Copy" on a copy button, a result label that only exists once you've clicked something.&lt;/p&gt;

&lt;p&gt;They exist separately for a boring, correct reason. The second one gets serialised into every page as a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; blob, so it holds only what the browser genuinely needs. Putting the server-side labels in there would ship a few hundred strings to every visitor for nothing.&lt;/p&gt;

&lt;p&gt;I diffed against the second one. The view reads the first.&lt;/p&gt;

&lt;p&gt;The real gap was 55 strings, and the mechanism that filled them had existed and worked for months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every step was defensible
&lt;/h2&gt;

&lt;p&gt;This is the part I keep turning over.&lt;/p&gt;

&lt;p&gt;Nothing I did was sloppy. I read the registry that defines the tools. I read the catalogue that holds the translations. I wrote a set difference. I sorted the output and eyeballed it, and the strings in it were plainly English — &lt;code&gt;Cost factor&lt;/code&gt;, &lt;code&gt;Background color&lt;/code&gt;, &lt;code&gt;Algorithm&lt;/code&gt; — which is exactly what a missing translation looks like.&lt;/p&gt;

&lt;p&gt;At no point did the evidence contradict itself. The numbers agreed with each other all the way down, because they were all answering the same wrong question: &lt;em&gt;is this string in that file?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The question I actually had was: &lt;strong&gt;does a Spanish visitor see this in Spanish?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One HTTP request would have answered it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/es/documents/rotate-pdf | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;label[^&amp;gt;]*&amp;gt;[^&amp;lt;]*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Rotación&lt;/code&gt;. &lt;code&gt;90° en sentido horario&lt;/code&gt;. Already there, in the first minute, instead of the fourth hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  It got worse before it got better
&lt;/h2&gt;

&lt;p&gt;Having "found" the gap, I fixed it — by changing the view to look strings up in the JavaScript dictionary as well.&lt;/p&gt;

&lt;p&gt;That looked like it worked. Pages rendered, labels appeared, nothing broke.&lt;/p&gt;

&lt;p&gt;What it actually did was give the application two dictionaries answering the same question, and they did not agree. The English string &lt;code&gt;Order&lt;/code&gt; was translated as 顺序 in one and 排序 in the other. Both are correct Chinese. Which one a visitor saw came down to which lookup ran first — an accident of the order of two lines in a template.&lt;/p&gt;

&lt;p&gt;I only caught it because a page showed me 顺序 while the file I had just edited said 排序, and those two facts could not both be true.&lt;/p&gt;

&lt;p&gt;That is the second lesson hiding inside the first: a fix built on a wrong diagnosis does not fail loudly. It produces a system that works and is wrong, which is considerably harder to find later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same week, twice more
&lt;/h2&gt;

&lt;p&gt;Once you have a name for a mistake you start seeing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two new pages, no internal links.&lt;/strong&gt; I added two tools to the site and checked whether anything linked to them, by reading the &lt;code&gt;related&lt;/code&gt; arrays in the data file. Zero. I was about to spend an hour adding links by hand when I loaded one of the existing pages instead. The links were already there — the registry's helper pads a short &lt;code&gt;related&lt;/code&gt; list with other tools from the same category, and every page in that category already pointed at the new ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A backlink that does not exist.&lt;/strong&gt; I was about to submit the site's API to a directory, and the question I set out to answer was whether their listing gives a &lt;code&gt;dofollow&lt;/code&gt; or a &lt;code&gt;nofollow&lt;/code&gt; link. I fetched a real listing page. 230 KB of HTML, and not one link to the provider's own site. The provider's domain does not appear anywhere on the page. Their profile URL 404s. &lt;code&gt;robots.txt&lt;/code&gt; disallows the whole &lt;code&gt;/provider/&lt;/code&gt; path.&lt;/p&gt;

&lt;p&gt;The question was not dofollow-or-nofollow. There is no link. I had spent my effort on a refinement of a premise I never checked.&lt;/p&gt;

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

&lt;p&gt;The tempting lesson is "always verify manually," and that is both true and useless — you cannot manually verify everything, and reading files is how most work gets done.&lt;/p&gt;

&lt;p&gt;The useful version is narrower:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every question has an artifact that is the ground truth for it, and it is often not the artifact you are already holding.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Is this string in the catalogue?&lt;/em&gt; → the catalogue is ground truth. Read the file.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Does the user see this translated?&lt;/em&gt; → the rendered page is ground truth. Read the page.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Does this page link to that one?&lt;/em&gt; → the rendered page, not the config that generates it.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Does this site give me a link?&lt;/em&gt; → their rendered page, not their marketing copy about their programme.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The failure in all three cases was substituting a source that was &lt;em&gt;upstream&lt;/em&gt; of the truth for the truth itself. Upstream sources are seductive because they are structured, greppable, and fast. The rendered page is none of those things, which is exactly why it is the one that settles arguments.&lt;/p&gt;

&lt;p&gt;A cheap habit that would have caught all three: before writing the script that measures the thing, load the thing once and look at it. Not as a test — as a sanity check on what you are about to spend the evening measuring.&lt;/p&gt;

&lt;p&gt;Fifty-five strings needed translating. I found that out the second time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://utilorax.com" rel="noopener noreferrer"&gt;Utilorax&lt;/a&gt;, a set of free browser-based tools. It is five languages deep, which is how you end up with two translation dictionaries and forget one of them exists.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>php</category>
      <category>i18n</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Google said 51 pages were excluded by noindex. Here's how I checked in ten minutes.</title>
      <dc:creator>Hammad Shams Uddin</dc:creator>
      <pubDate>Sat, 08 Aug 2026 11:38:29 +0000</pubDate>
      <link>https://dev.to/hammad4june1999/google-said-51-pages-were-excluded-by-noindex-heres-how-i-checked-in-ten-minutes-36mg</link>
      <guid>https://dev.to/hammad4june1999/google-said-51-pages-were-excluded-by-noindex-heres-how-i-checked-in-ten-minutes-36mg</guid>
      <description>&lt;p&gt;Two emails from Search Console arrived the same morning. The first was the one that worried me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;New reasons prevent pages in a sitemap from being indexed on site utilorax.com&lt;/strong&gt;&lt;br&gt;
Excluded by 'noindex' tag&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read that carefully. Pages &lt;em&gt;in a sitemap&lt;/em&gt; — pages I am explicitly asking Google to index — are being skipped because they carry a &lt;code&gt;noindex&lt;/code&gt;. That is a contradiction, and it is the kind that quietly costs you: the sitemap says "index this", the page says "don't", and Google believes the page.&lt;/p&gt;

&lt;p&gt;Fifty-one of them. My site has around 6,000 URLs across five languages, so 51 was small enough to be a real bug and large enough to matter.&lt;/p&gt;

&lt;p&gt;Here is how I found out whether it was one, without opening 51 URLs by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The method: sample by URL shape, not at random
&lt;/h2&gt;

&lt;p&gt;Random sampling is the wrong tool here. A &lt;code&gt;noindex&lt;/code&gt; bug is almost never random — it comes from a template, a route, or a language variant, so it affects a whole &lt;em&gt;class&lt;/em&gt; of URLs. If you sample 50 URLs out of 6,000 at random you will probably miss a class of 51. If you sample one URL from every distinct shape, you cannot.&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Collect every URL the sitemaps actually claim.&lt;/strong&gt; Not what you think you publish — what the XML says today.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/sitemap.xml &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;loc&amp;gt;[^&amp;lt;]*&amp;lt;/loc&amp;gt;'&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s|&amp;lt;/\?loc&amp;gt;||g'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; maps.txt

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;m&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--retry&lt;/span&gt; 2 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;loc&amp;gt;[^&amp;lt;]*&amp;lt;/loc&amp;gt;'&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s|&amp;lt;/\?loc&amp;gt;||g'&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; maps.txt | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; allurls.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gave me 5,881 URLs across 54 sitemap files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Reduce them to shapes.&lt;/strong&gt; Strip the language prefix, keep the first path segment, mark anything deeper with a wildcard. &lt;code&gt;/es/convert/miles-to-km&lt;/code&gt; and &lt;code&gt;/es/convert/kg-to-lb&lt;/code&gt; are the same shape; &lt;code&gt;/es/blog/some-post&lt;/code&gt; is a different one.&lt;/p&gt;

&lt;p&gt;5,881 URLs collapsed into &lt;strong&gt;101 distinct shapes&lt;/strong&gt; — &lt;code&gt;en /convert/*&lt;/code&gt;, &lt;code&gt;es /convert/*&lt;/code&gt;, &lt;code&gt;fr /tools/*&lt;/code&gt;, &lt;code&gt;en /about&lt;/code&gt;, and so on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Fetch one of each and check both places a &lt;code&gt;noindex&lt;/code&gt; can hide.&lt;/strong&gt; This is the step people get wrong: they check the HTML meta tag and forget the header, or the reverse.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read &lt;/span&gt;u&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;H&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 30 &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; body.html &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;XR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$H&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"^x-robots-tag"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;MR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;meta name="robots" content="[^"]*"'&lt;/span&gt; body.html | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$XR&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$MR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qi&lt;/span&gt; &lt;span class="s2"&gt;"noindex"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"HIT: &lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
done&lt;/span&gt; &amp;lt; sample.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;strong&gt;101 out of 101 returned 200, and not one carried a &lt;code&gt;noindex&lt;/code&gt;&lt;/strong&gt; — in the header or in the markup. No contradiction. The sitemap and the robots directives agreed everywhere.&lt;/p&gt;

&lt;p&gt;Ten minutes, and the scary email was answered.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what were the 51?
&lt;/h2&gt;

&lt;p&gt;Once the sitemap was cleared, the answer had to be pages Google found some other way — through internal links, old crawls, or the wider web. Grepping my own routing for every place that sets a &lt;code&gt;noindex&lt;/code&gt; gave the list immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/api/*&lt;/code&gt; — JSON endpoints&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/cron/*&lt;/code&gt; — scheduled jobs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/dl/*&lt;/code&gt; — file downloads&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/developers/*&lt;/code&gt; — logged-in account pages&lt;/li&gt;
&lt;li&gt;and eight pages I had to work out: the &lt;code&gt;es&lt;/code&gt;, &lt;code&gt;fr&lt;/code&gt;, &lt;code&gt;pt&lt;/code&gt; and &lt;code&gt;zh&lt;/code&gt; versions of &lt;code&gt;/refund&lt;/code&gt; and &lt;code&gt;/terms&lt;/code&gt;, which have no translation and are deliberately &lt;code&gt;noindex&lt;/code&gt; outside English until someone writes one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of them is &lt;code&gt;noindex&lt;/code&gt; on purpose. Google was not reporting a bug. It was reporting a fact, in a message worded strongly enough to look like a bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That is the part worth keeping.&lt;/strong&gt; "Excluded by 'noindex' tag" is not an error. It is Search Console telling you what it found, and on any site with an admin area, an API or logged-in pages, a non-zero number there is correct and healthy. The number to react to is not the count — it is whether any of those URLs are ones you wanted indexed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other number I had been reading wrong for a week
&lt;/h2&gt;

&lt;p&gt;While I was in there, the Page Indexing report finished processing for the first time since launch. It said:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Indexed        3,040
Not indexed    3,237
Total known    6,277        -&amp;gt;  48.4% indexed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My notes said 12%.&lt;/p&gt;

&lt;p&gt;That figure had come from the &lt;strong&gt;HTTPS report&lt;/strong&gt;, which showed 728 URLs at a time when the indexing report was stuck on "processing". I had divided 728 by my sitemap total and written down "only 12% is indexed, so indexing is the bottleneck" — and then let that sentence drive my priorities for a week.&lt;/p&gt;

&lt;p&gt;The HTTPS report counts URLs whose HTTPS status Google evaluated. It is a sample. It was never the index size, and it never claimed to be. I had made it answer a question it was not asked.&lt;/p&gt;

&lt;p&gt;Half of a twelve-day-old site being indexed is not a bottleneck. It is fine. The actual bottleneck was somewhere else entirely, and I had spent a week not looking at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one that was genuinely worth opening
&lt;/h2&gt;

&lt;p&gt;The bigger bucket was &lt;strong&gt;"Crawled — currently not indexed": 215 pages.&lt;/strong&gt; That one means Google fetched the page, read it, and decided against it — which is a judgement, not a mechanic, and worth understanding.&lt;/p&gt;

&lt;p&gt;I pulled the full list and counted it rather than eyeballing the examples:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Non-English&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;97%&lt;/strong&gt; (es 38%, fr 35%, pt 23%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/convert/*&lt;/code&gt; pages&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;71%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So: non-English converter pages, almost exclusively.&lt;/p&gt;

&lt;p&gt;The obvious theory is thin or machine-translated content. I tested it by comparing a Spanish page against its English counterpart word by word — they share only &lt;strong&gt;17–21%&lt;/strong&gt; of their vocabulary and each runs about 1,000 words. The translations are real.&lt;/p&gt;

&lt;p&gt;What I did find was worse in a subtler way: three places built an &lt;strong&gt;English sentence and dropped translated nouns into the gaps&lt;/strong&gt;. The breadcrumb read "Caballos de Fuerza &lt;strong&gt;to&lt;/strong&gt; Vatios". Related cards read "Convert Caballos de Fuerza &lt;strong&gt;to&lt;/strong&gt; Kilovatios &lt;strong&gt;instantly&lt;/strong&gt;." Eleven English fragments per page, on every converter, in four languages.&lt;/p&gt;

&lt;p&gt;That is a genuine quality bug and I fixed it. But I want to be honest about what fixing it does, because the temptation is to declare victory: &lt;strong&gt;it is almost certainly not why those pages were skipped.&lt;/strong&gt; A twelve-day-old domain with zero external links does not get unlimited index budget, and non-English pages of a site nobody has ever linked to are the first thing dropped. That is an authority problem. No amount of copy editing fixes it — links and time do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell myself a week ago
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sample by URL shape, not at random.&lt;/strong&gt; Template bugs affect classes, so test classes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the header and the meta tag.&lt;/strong&gt; A &lt;code&gt;noindex&lt;/code&gt; can live in either, and the report does not tell you which.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Excluded by noindex" is usually correct.&lt;/strong&gt; Verify it is only hitting things you meant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never infer one report's number from another's.&lt;/strong&gt; The HTTPS report is not the index count. A number that is roughly the right shape is the easiest kind of wrong to keep believing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When you find a real bug during an investigation, do not let it become the answer.&lt;/strong&gt; It felt like the cause. It was not.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://utilorax.com" rel="noopener noreferrer"&gt;Utilorax&lt;/a&gt;, a set of free browser-based tools. Everything runs client-side, which produces a steady supply of problems like this one.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>googlesearchconsole</category>
      <category>programming</category>
    </item>
    <item>
      <title>Parallel connections made my speed test 2x slower</title>
      <dc:creator>Hammad Shams Uddin</dc:creator>
      <pubDate>Sat, 08 Aug 2026 10:18:06 +0000</pubDate>
      <link>https://dev.to/hammad4june1999/parallel-connections-made-my-speed-test-2x-slower-4dne</link>
      <guid>https://dev.to/hammad4june1999/parallel-connections-made-my-speed-test-2x-slower-4dne</guid>
      <description>&lt;p&gt;I built a speed test last week. It told me my download was &lt;strong&gt;8.2 Mbps&lt;/strong&gt;. Speedtest.net, on the same line, five minutes later, said &lt;strong&gt;28.65&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;My first instinct was that this was a distance problem, and I had a good reason to think so. Speedtest.net had picked a PTCL server in Karachi — same city, &lt;strong&gt;3 ms&lt;/strong&gt; away. Mine measures to whichever Cloudflare edge answers, and from here that is Singapore, &lt;strong&gt;120 ms&lt;/strong&gt; away. Of course a server on another continent reads lower.&lt;/p&gt;

&lt;p&gt;That explanation was comfortable, it fit the numbers, and it was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement that killed it
&lt;/h2&gt;

&lt;p&gt;Before rewriting anything, I checked the assumption with curl. Same machine, same file, same Cloudflare edge, all within a minute of each other:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;how the file was fetched&lt;/th&gt;
&lt;th&gt;throughput&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;one connection at a time, sequentially&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19.2 Mbps&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;six parallel connections&lt;/td&gt;
&lt;td&gt;11.1 Mbps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;my speed test, eight HTTP/2 streams&lt;/td&gt;
&lt;td&gt;8.2 Mbps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The path was capable of 19 Mbps. My tool was reporting 8. And the more connections I opened, the worse it got — the exact opposite of what I had built the thing to assume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I had it backwards
&lt;/h2&gt;

&lt;p&gt;Every guide on measuring bandwidth says the same thing, and it is not wrong: a single TCP connection often cannot fill a long fat pipe. Throughput per connection is bounded by the congestion window divided by the round trip time, so on a 120 ms path one connection tops out well below the line rate no matter how fast the line is. Open several and you fill the pipe.&lt;/p&gt;

&lt;p&gt;I had believed this hard enough that, an hour before running that curl test, I had &lt;em&gt;tuned the default from six streams to eight&lt;/em&gt; — reasoning that a long path needs more parallelism. I made it worse and felt clever doing it.&lt;/p&gt;

&lt;p&gt;What that advice omits is that it assumes spare capacity. Parallel connections do not politely share a fixed pot of bandwidth; each one runs its own congestion control and pushes until it sees loss. On an uncongested path they ramp independently and add up. On a path that is &lt;em&gt;already&lt;/em&gt; the bottleneck — an oversubscribed international link, in my case — they push it into loss together, every one of them backs off, and the aggregate collapses below what a single well-behaved connection was getting.&lt;/p&gt;

&lt;p&gt;Both behaviours are real. Which one you get depends on the path, and you cannot know which from the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  So stop guessing and measure it
&lt;/h2&gt;

&lt;p&gt;The fix was to stop picking a number. The download phase now runs twice — once with two connections, once with eight — and reports whichever went faster, showing both.&lt;/p&gt;

&lt;p&gt;On my line, live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2 connections : 15.1 Mbps
8 connections :  5.18 Mbps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same file, same run, ninety seconds apart. Three times the difference, purely from concurrency.&lt;/p&gt;

&lt;p&gt;The reported download went from 8.2 to &lt;strong&gt;15.1 Mbps&lt;/strong&gt;. Still below Ookla's 28, and I will come back to why, but it is now measuring the connection rather than measuring my assumption about the connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways I rigged my own experiment
&lt;/h2&gt;

&lt;p&gt;The first version of that A/B was not a fair test, in two ways that are worth naming because both are easy to repeat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The second segment inherited the first one's congestion.&lt;/strong&gt; Five seconds of saturated download leaves queues full along the path. Starting the next measurement immediately measures the tail of the previous one, which makes whichever segment ran second look worse regardless of what it was doing. There is now a 600 ms drain between them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The second segment got half the warm-up.&lt;/strong&gt; I had reasoned that the connection was already open and warm, so it needed less time to ramp. True of the connection — false of the six additional streams, which each start at a small congestion window and need the same number of round trips to open up. I was counting their ramp as their speed. Both segments now get the same warm-up.&lt;/p&gt;

&lt;p&gt;That warm-up is itself scaled to the measured ping rather than fixed, for the same reason: slow start doubles the congestion window once per round trip, so the time a connection needs to reach full speed is counted in RTTs, not seconds. A flat 1.4 seconds is generous on a 10 ms link and far too short on a 120 ms one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I could not fix
&lt;/h2&gt;

&lt;p&gt;The remaining gap against Ookla is genuinely the path, and I checked that too rather than assuming it a second time.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;speed.cloudflare.com&lt;/code&gt; gets &lt;strong&gt;24.7 Mbps&lt;/strong&gt; from this machine — because Cloudflare serves it from their &lt;strong&gt;Karachi&lt;/strong&gt; edge. My site is served from &lt;strong&gt;Singapore&lt;/strong&gt;. Same CDN, same city, different datacentre, and which one answers is Cloudflare's routing, not something a site owner sets.&lt;/p&gt;

&lt;p&gt;So the honest position is that both numbers are true and they are answers to different questions. Ookla's 28 is roughly the best this line does to a server inside the same ISP. My 15 is roughly what it does to a service hosted abroad — which, for most of what people actually load, is the more relevant number. The tool now says so on screen when the round trip is over 80 ms, instead of leaving people to conclude it is broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three smaller things that also decide whether the number means anything
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The payload has to be incompressible.&lt;/strong&gt; A file of zeros gets compressed in transit, so the browser receives a few kilobytes, inflates them, and reports a connection hundreds of times faster than it is. Random bytes, and I check the generated file's own compressibility rather than trusting the RNG.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The file extension decides whether it is edge-cached.&lt;/strong&gt; I measured this rather than reading it: on my domain, &lt;code&gt;.bin&lt;/code&gt;, &lt;code&gt;.zip&lt;/code&gt;, &lt;code&gt;.gz&lt;/code&gt;, &lt;code&gt;.jpg&lt;/code&gt; and &lt;code&gt;.woff2&lt;/code&gt; come back &lt;code&gt;cf-cache-status: HIT&lt;/code&gt;, while &lt;code&gt;.dat&lt;/code&gt;, &lt;code&gt;.br&lt;/code&gt; and &lt;code&gt;.wasm&lt;/code&gt; come back &lt;code&gt;DYNAMIC&lt;/code&gt;. A DYNAMIC payload means every request travels to the origin, so you end up measuring a shared host in one fixed country instead of the user's path to the edge — and paying for the bandwidth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;xhr.upload.onprogress&lt;/code&gt; does not measure the wire.&lt;/strong&gt; It fires when data enters the operating system's send buffer, which swallows several megabytes instantly. Mine reported a 90 Mbps "peak" beside a 26 Mbps average on the same eight seconds. Nobody reading those two numbers concludes they were measured differently; they conclude the tool is broken. Every upload figure now comes from byte counts the server confirms receiving.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The advice that more connections are faster is correct often enough to be repeated everywhere and to sound like a law. It is a heuristic about one common case, and on a congested path it inverts completely.&lt;/p&gt;

&lt;p&gt;If you are measuring throughput, the general form of the lesson is worth more than the specific finding: &lt;strong&gt;when a parameter's correct value depends on conditions you cannot observe from your side, measure both and keep the winner.&lt;/strong&gt; It costs one extra segment and removes an assumption that was silently halving my results.&lt;/p&gt;

&lt;p&gt;The test is &lt;a href="https://utilorax.com/tools/internet-speed-test" rel="noopener noreferrer"&gt;here&lt;/a&gt; if you want to see the two-segment result on your own line — it shows both figures, so you can see which way your path leans.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://utilorax.com" rel="noopener noreferrer"&gt;Utilorax&lt;/a&gt;, a set of free browser-based tools. Everything runs client-side, which produces a steady supply of problems like this one.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>networking</category>
      <category>javascript</category>
    </item>
    <item>
      <title>ffmpeg.wasm 0.12 hung on the first frame — and the real speedup was not the upgrade</title>
      <dc:creator>Hammad Shams Uddin</dc:creator>
      <pubDate>Thu, 06 Aug 2026 22:08:19 +0000</pubDate>
      <link>https://dev.to/hammad4june1999/ffmpegwasm-012-hung-on-the-first-frame-and-the-real-speedup-was-not-the-upgrade-p45</link>
      <guid>https://dev.to/hammad4june1999/ffmpegwasm-012-hung-on-the-first-frame-and-the-real-speedup-was-not-the-upgrade-p45</guid>
      <description>&lt;p&gt;I run &lt;a href="https://utilorax.com" rel="noopener noreferrer"&gt;Utilorax&lt;/a&gt;, a set of free browser-based tools. The video tools are ffmpeg.wasm, which means every encode runs on the user's own machine and nothing is ever uploaded. That is the whole point of them — and it is also why they were slow.&lt;/p&gt;

&lt;p&gt;This is what a week of trying to fix that actually taught me. The upgrade I was sure would fix it did not, and the thing that did was much less interesting and much more effective.&lt;/p&gt;

&lt;h2&gt;
  
  
  The starting point: one core
&lt;/h2&gt;

&lt;p&gt;Every encode was pinned to a single thread:&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;function&lt;/span&gt; &lt;span class="nf"&gt;pinThreads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;args&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;1&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;out&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 looks like an oversight. The core is the multi-threaded build, &lt;code&gt;ffmpeg-core.worker.js&lt;/code&gt; ships with it, and the page already pays for COOP/COEP so &lt;code&gt;SharedArrayBuffer&lt;/code&gt; is available. Letting ffmpeg use every core looks like free speed.&lt;/p&gt;

&lt;p&gt;It is not. Take the pin off on ffmpeg.wasm &lt;strong&gt;0.11&lt;/strong&gt; and the video filter dies instantly with &lt;code&gt;RuntimeError: function signature mismatch&lt;/code&gt; thrown out of &lt;code&gt;worker.js onmessage&lt;/code&gt; — a known 0.11 fault where the threaded core trips over its own dynCall table. Nothing renders. 0%.&lt;/p&gt;

&lt;p&gt;So the encoder ran on one core on machines that have eight. A 1080p60 clip filtered at about 0.09× real time. 0.12 fixed the threading fault, so 0.12 was obviously the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading to 0.12: three things nobody mentions
&lt;/h2&gt;

&lt;p&gt;The API changed shape, not just names — &lt;code&gt;run()&lt;/code&gt; became &lt;code&gt;exec()&lt;/code&gt;, the &lt;code&gt;FS()&lt;/code&gt; calls became methods, &lt;code&gt;fetchFile&lt;/code&gt; moved package, and everything returns a promise. Rather than rewrite sixty-one call sites twice (once to port, once to revert if it went wrong) I put an adapter in front that presents the 0.11 surface on top of 0.12. That part went fine.&lt;/p&gt;

&lt;p&gt;Then the interesting failures started.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The UMD bundle is not self-contained
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;@ffmpeg/ffmpeg&lt;/code&gt;'s UMD build loads its own worker as a &lt;strong&gt;separate webpack chunk&lt;/strong&gt;, &lt;code&gt;814.ffmpeg.js&lt;/code&gt;, from whatever directory &lt;code&gt;ffmpeg.js&lt;/code&gt; came from. It is not in the docs and it is not obvious — you just get a 404 and a &lt;code&gt;load()&lt;/code&gt; that never resolves. Ship it alongside &lt;code&gt;ffmpeg.js&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;classWorkerURL&lt;/code&gt; looks like the fix for that. It is a trap.
&lt;/h3&gt;

&lt;p&gt;Passing it sends the UMD build down its ESM branch:&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="nx"&gt;load&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;classWorkerURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file:///home/jeromewu/…&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;module&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;   &lt;span class="c1"&gt;// ← ESM branch&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;u&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;814&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;          &lt;span class="c1"&gt;// ← classic&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;module&lt;/strong&gt; worker — around a chunk that is a classic script calling &lt;code&gt;importScripts()&lt;/code&gt;. It dies on load. Leaving the option out is what makes it work.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Workers need the COEP header on themselves
&lt;/h3&gt;

&lt;p&gt;A cross-origin-isolated page will refuse a worker script unless &lt;strong&gt;that script's own response&lt;/strong&gt; carries &lt;code&gt;Cross-Origin-Embedder-Policy: require-corp&lt;/code&gt;. Same-origin is not enough. Apache sends nothing on a static file, so the whole chain fails: the class worker, the core it &lt;code&gt;importScripts&lt;/code&gt;, and the pthread workers Emscripten spawns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;If&lt;/span&gt;&lt;span class="sr"&gt; "%{REQUEST_URI} =~ m#/js/vendor/ffmpeg-012/#"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Cross-Origin-Embedder-Policy "require-corp"
    &lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Cross-Origin-Resource-Policy "same-origin"
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;If&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;0.11 never needed this because it builds its worker from a &lt;strong&gt;blob URL&lt;/strong&gt;, and a blob inherits the page's policies instead of being checked for headers. That is a difference in how the worker is created, not in the API — you will not find it by reading the diff.&lt;/p&gt;

&lt;p&gt;One more: keep your service worker away from those files. It caches whatever headers an entry had when it was written, so one fetch made before the header rule existed gets served back forever, and it surfaces as a COEP error pointing at the worker rather than at the cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then it hung on the first frame
&lt;/h2&gt;

&lt;p&gt;Headers fixed, pin removed, and the filter printed exactly one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;frame=    1 fps=0.0 q=0.0 size= 0kB time=00:00:00.14 bitrate= 2.6kbits/s speed=2.04x
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then nothing. No error. 0% forever.&lt;/p&gt;

&lt;p&gt;The core pre-allocates &lt;strong&gt;exactly 32 pthread workers&lt;/strong&gt; and cannot grow past them here:&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="nx"&gt;getNewWorker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;function &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;PThread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unusedWorkers&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;PThread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allocateUnusedWorker&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;PThread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadWasmModuleToWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PThread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unusedWorkers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;  &lt;span class="c1"&gt;// ← async&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;…&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Growing the pool means loading the module into a fresh worker and &lt;strong&gt;waiting for it to answer&lt;/strong&gt; — but ffmpeg runs synchronously inside the very worker that would have to process that answer. The thread it is blocked on can never start. Ask for a 33rd and everything stops, silently.&lt;/p&gt;

&lt;p&gt;Left to itself ffmpeg asks for far more than 32. It spends threads in three places at once: one set per input decoder, one for the filter graph, and x264 takes 1.5 per core on top. On anything with nine or more logical cores you are already over the line — which is why this only appears once the pin comes off.&lt;/p&gt;

&lt;p&gt;So: give each stage an explicit budget instead of the machine's core count.&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;var&lt;/span&gt; &lt;span class="nx"&gt;THREADS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hardwareConcurrency&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// worst case ≈ (inputs + 2) × THREADS — a 5-clip merge at 4 is already 28 of the 32&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-filter_threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-filter_complex_threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-filter_threads&lt;/code&gt; matters more than it looks. It is a global option and defaults to one thread per core on its own, which was most of the overspend.&lt;/p&gt;

&lt;h2&gt;
  
  
  The error that ate its own error message
&lt;/h2&gt;

&lt;p&gt;Two ops aborted instead of hanging, and every one of them reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TypeError: Cannot read properties of undefined (reading 'startsWith')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not your bug. It is the core's own error handler:&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_ffmpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;stringsToPtr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Aborted&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="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&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;&lt;code&gt;e&lt;/code&gt; is not always an &lt;code&gt;Error&lt;/code&gt;. When it isn't, &lt;code&gt;e.message&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt;, and the handler throws while inspecting the crash — so whatever ffmpeg actually said is lost. If you see that string, the real failure is upstream and you cannot see it.&lt;/p&gt;

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

&lt;p&gt;I put the tools back on 0.11.&lt;/p&gt;

&lt;p&gt;Not because 0.12 is bad, but because the premise was wrong. 0.12 is worth a few times one core. A 6-minute 1080p60 clip is about 21,000 frames, and software x264 in wasm will not get through that quickly at any thread count. I was optimising the wrong axis.&lt;/p&gt;

&lt;p&gt;The real win was &lt;strong&gt;not running the encoder at all&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Container conversion&lt;/strong&gt; — MKV→MP4 and friends are the same H.264 in a different box. &lt;code&gt;-c copy&lt;/code&gt; instead of decode-and-re-encode: seconds, and no second round of lossy compression. Judge the &lt;em&gt;output&lt;/em&gt;, not the promise — ffmpeg.wasm resolves even when ffmpeg gave up, so try the copy and fall back if the file is missing or tiny. Trying the copy &lt;strong&gt;is&lt;/strong&gt; the probe, and it is cheaper than the transcode it replaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate&lt;/strong&gt; — a quarter turn is a flag on the video track, not a change to the pixels. Stream copy plus &lt;code&gt;-metadata:s:v:0 rotate=90&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trim&lt;/strong&gt; — copy the streams instead of rebuilding them. The cut then lands on a keyframe, so it is a real trade rather than a free win: make it a checkbox.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rotate has a catch worth knowing. The risk is not that the copy fails, it is that it &lt;strong&gt;silently does nothing&lt;/strong&gt;: phone footage often already carries a rotation, and when the streams are copied that existing flag can survive and override the one you set. The file comes out perfectly valid, and unturned. So measure the output rather than trusting it — a &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; applies a container's rotation when it reports &lt;code&gt;videoWidth&lt;/code&gt;, so a turn that worked has the sides swapped:&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;function&lt;/span&gt; &lt;span class="nf"&gt;displayedSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;settle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revokeObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&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="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// an answer that never comes is the freeze you were avoiding&lt;/span&gt;
    &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onloadedmetadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;videoWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;videoHeight&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&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;No swap, no proof, re-encode.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell myself a week ago
&lt;/h2&gt;

&lt;p&gt;Threads were the interesting problem. Not encoding was the useful one. The ops that got fast — &lt;a href="https://utilorax.com/documents/video-convert" rel="noopener noreferrer"&gt;convert&lt;/a&gt;, &lt;a href="https://utilorax.com/documents/video-rotate" rel="noopener noreferrer"&gt;rotate&lt;/a&gt;, trim, mute — got fast because they stopped doing work, not because they did it on more cores.&lt;/p&gt;

&lt;p&gt;And when the number of threads is the thing you are tuning, check what the runtime can actually give you before you ask for more. A pool of 32 that cannot grow is a hard ceiling, and the failure mode is a silent hang rather than an error — which is a bad way to find the edge.&lt;/p&gt;

&lt;p&gt;For the ops that genuinely must decode every frame — filters, resize, compress — wasm is still wasm, and the next honest step is WebCodecs and the hardware encoder. That is a different post.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>webassembly</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
