<?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>Forem: Marsha Teo</title>
    <description>The latest articles on Forem by Marsha Teo (@marshateo).</description>
    <link>https://forem.com/marshateo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3862667%2F204e9381-60ad-4ef3-bd15-5d4a3bdcfb60.png</url>
      <title>Forem: Marsha Teo</title>
      <link>https://forem.com/marshateo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/marshateo"/>
    <language>en</language>
    <item>
      <title>The Scheduling Boundaries Behind Responsive UI</title>
      <dc:creator>Marsha Teo</dc:creator>
      <pubDate>Mon, 18 May 2026 05:35:33 +0000</pubDate>
      <link>https://forem.com/marshateo/the-scheduling-boundaries-behind-responsive-ui-2m56</link>
      <guid>https://forem.com/marshateo/the-scheduling-boundaries-behind-responsive-ui-2m56</guid>
      <description>&lt;p&gt;This is the last article in a series on how JavaScript actually runs. You can read the full series &lt;a href="https://dev.to/marshateo/javascript-event-loop-series-building-the-event-loop-mental-model-from-experiments-4d8i"&gt;here&lt;/a&gt; or on my &lt;a href="https://www.marshateo.com/writing/javascript-event-loop-landing" rel="noopener noreferrer"&gt;website&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;We now know how the event loop and rendering pipeline behave.  &lt;/p&gt;

&lt;p&gt;The browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs a macrotask to completion.&lt;/li&gt;
&lt;li&gt;Drains all microtasks.&lt;/li&gt;
&lt;li&gt;Executes any scheduled &lt;code&gt;requestAnimationFrame&lt;/code&gt; callbacks.&lt;/li&gt;
&lt;li&gt;Drains all microtasks.&lt;/li&gt;
&lt;li&gt;Performs layout and paint to produce the next frame.&lt;/li&gt;
&lt;li&gt;Moves on to the next macrotask.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Given that environment, how should we write UI code?&lt;/p&gt;




&lt;h2&gt;
  
  
  Long Tasks Block Everything
&lt;/h2&gt;

&lt;p&gt;If you want the UI to stay responsive, your tasks must yield quickly.&lt;/p&gt;

&lt;p&gt;Consider 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="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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="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;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// busy loop for 3 seconds&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;done&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;Once the button is clicked, the page becomes unresponsive. The click handler is a macrotask and nothing can interrupt it. Everything else has to wait: there is no re-rendering, no new input, no &lt;code&gt;requestAnimationFrame&lt;/code&gt; callbacks. &lt;/p&gt;

&lt;p&gt;Long-running tasks monopolize the main thread. While they run, rendering pauses, input waits, animations stall and timers are delayed. Responsive UI depends on cooperation. &lt;/p&gt;




&lt;h2&gt;
  
  
  Microtasks Do Not Yield to Rendering
&lt;/h2&gt;

&lt;p&gt;Chaining promises look like a way to break work into pieces:&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;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;Promise&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="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="nf"&gt;heavyWork&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="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Halfway...&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="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="nf"&gt;moreHeavyWork&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="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Done&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, each &lt;code&gt;.then()&lt;/code&gt; callback becomes a microtask when its associated &lt;code&gt;Promise&lt;/code&gt; resolves. Because the browser must drain the entire microtask queue before rendering, chaining Promises does not create render opportunities. As a result, the user sees nothing until &lt;code&gt;"Done"&lt;/code&gt; shows up on screen. &lt;/p&gt;

&lt;p&gt;If you want to let the browser render, you must introduce a scheduling boundary:&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;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;Promise&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="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="nf"&gt;heavyWork&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="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Halfway...&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="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="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// setTimeout used to introduce a scheduling boundary&lt;/span&gt;
        &lt;span class="nf"&gt;setTimeout&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="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="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="nf"&gt;heavyWork&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="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Done&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, we can consider:&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;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;Promise&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="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="nf"&gt;heavyWork&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="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Halfway...&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="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="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// requestAnimationFrame used to introduce a scheduling boundary&lt;/span&gt;
        &lt;span class="nf"&gt;requestAnimationFrame&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="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="nf"&gt;heavyWork&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="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Done&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both approaches work because a &lt;code&gt;.then()&lt;/code&gt; callback is only queued once its associated &lt;code&gt;Promise&lt;/code&gt; resolves. By returning a &lt;code&gt;Promise&lt;/code&gt; that resolves later, we delay when the next microtask is created. &lt;code&gt;setTimeout&lt;/code&gt; yields to the next macrotask, while  &lt;code&gt;requestAnimationFrame&lt;/code&gt; yields to the next frame. &lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing Between Promises, &lt;code&gt;setTimeout&lt;/code&gt; and &lt;code&gt;requestAnimationFrame&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;These mechanisms signal different intentions to the browser. They are not interchangeable.&lt;/p&gt;

&lt;p&gt;Use a &lt;code&gt;Promise&lt;/code&gt; when you need to continue work immediately after the current macrotask completes, but before the browser moves on. They are ideal for continuing work that logically depends on previous work. They help to preserve order, transform results and update state after completion. They are not a yielding mechanism. &lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;setTimeout&lt;/code&gt; when you need to create a real scheduling gap. They are useful for breaking up long computation, deferring non-critical work and yielding cooperatively. They are general purpose yields. &lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;requestAnimationFrame&lt;/code&gt; when you are performing visual updates. It is ideal for animations and layout-sensitive work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy0ero3mkgz39c6c4bs7l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy0ero3mkgz39c6c4bs7l.png" alt="Decision tree to decide among Promises, setTimeout and requestAnimationFrame" width="796" height="500"&gt;&lt;/a&gt;&lt;br&gt;Different scheduling APIs solve different coordination problems in the browser event loop.
  &lt;/p&gt;




&lt;h2&gt;
  
  
  Align Visual Updates to Frames
&lt;/h2&gt;

&lt;p&gt;Consider this approach:&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mousemove&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;event&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="nx"&gt;box&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;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px&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;If the mouse fires 200 events per second, this code attempts 200 DOM updates per second. But on most displays, the refresh happens about 60 times per second. Visual work that exceeds that rate is simply wasted.&lt;/p&gt;

&lt;p&gt;Instead, consider:&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;latestX&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;scheduled&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mousemove&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;event&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="nx"&gt;latestX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientX&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;scheduled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;scheduled&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="nf"&gt;requestAnimationFrame&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="nx"&gt;box&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;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;latestX&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;scheduled&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="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;We use &lt;code&gt;requestAnimationFrame&lt;/code&gt; to update at most once per frame, no matter the rate at which input can fire. &lt;/p&gt;




&lt;h2&gt;
  
  
  Respect the Frame Budget
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt; guarantees alignment to the frames but it does not guarantee smoothness. On a 60fps display, the browser has roughly 16ms per frames. That 16ms must include all JavaScript and rendering work. &lt;/p&gt;

&lt;p&gt;If the JavaScript executed alone takes longer than that, the browser cannot complete rendering in time:&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;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 40ms of work&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;This callback runs before the browser renders but it blocks for 40ms and therefore exceeds the frame budget. Since the browser cannot display partial frames, if the 16ms window is missed, that frame is dropped. Instead of rendering at 60 frames per second, the browser renders less frequently: Animations can appear jerky, motion uneven and interactions delayed.  &lt;/p&gt;

&lt;p&gt;So while &lt;code&gt;requestAnimationFrame&lt;/code&gt; helped with alignment, we must still finish the frame work within the frame window. Either work fits inside the budget or it is spread across multiple frames. For instance, this could mean animating in steps or deferring non-critical computation. &lt;/p&gt;

&lt;p&gt;Responsive UI requires both correct scheduling and work that fits inside the budget. &lt;/p&gt;




&lt;h2&gt;
  
  
  Guard Against Stale Asynchronous Work
&lt;/h2&gt;

&lt;p&gt;Asynchronous code creates delays. During this window, state can 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;function&lt;/span&gt; &lt;span class="nf"&gt;loadData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/data&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&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;data&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;This looks harmless but imagine the user clicking twice quickly and &lt;code&gt;loadData()&lt;/code&gt; is called twice in succession. If the second request finishes first, the first request would render stale data and the UI would then be incorrect. &lt;/p&gt;

&lt;p&gt;One common pattern is to guard against outdated work:&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;currentRequestId&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadData&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;currentRequestId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/data&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;currentRequestId&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="nf"&gt;render&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now each request captures its own response and only the most recent request is allowed to update the UI. &lt;/p&gt;




&lt;h2&gt;
  
  
  Designing With the Browser, Not Against It
&lt;/h2&gt;

&lt;p&gt;Responsive UI emerges from working within the browser's execution model. &lt;/p&gt;

&lt;p&gt;In practice, that often means: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keeping tasks short so the browser can continue scheduling&lt;/li&gt;
&lt;li&gt;Remembering that microtasks do not yield to rendering&lt;/li&gt;
&lt;li&gt;Aligning visual updates to frame boundaries&lt;/li&gt;
&lt;li&gt;Ensuring that work fits within the frame budget.&lt;/li&gt;
&lt;li&gt;Verifying that delayed work is still relevant before applying it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I started this series because I had code that used Promises, &lt;code&gt;setTimeout&lt;/code&gt;, and &lt;code&gt;requestAnimationFrame&lt;/code&gt;. They all felt “asynchronous” and interchangeable. Turns out they weren't. &lt;/p&gt;

&lt;p&gt;Good UI code knows which scheduling boundary to use and when.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on my &lt;a href="https://www.marshateo.com/writing/event-loop-application" rel="noopener noreferrer"&gt;website&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>requestAnimationFrame: The Missing Scheduling Layer</title>
      <dc:creator>Marsha Teo</dc:creator>
      <pubDate>Fri, 08 May 2026 16:50:01 +0000</pubDate>
      <link>https://forem.com/marshateo/requestanimationframe-the-missing-scheduling-layer-1el0</link>
      <guid>https://forem.com/marshateo/requestanimationframe-the-missing-scheduling-layer-1el0</guid>
      <description>&lt;p&gt;This is the sixth article in a series on how JavaScript actually runs. You can read the full series &lt;a href="https://dev.to/marshateo/javascript-event-loop-series-building-the-event-loop-mental-model-from-experiments-4d8i"&gt;here&lt;/a&gt; or on my &lt;a href="https://www.marshateo.com/writing/javascript-event-loop-landing" rel="noopener noreferrer"&gt;website&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;In the &lt;a href="https://dev.to/marshateo/rendering-is-a-browser-decision-not-a-javascript-one-47e3"&gt;last article&lt;/a&gt;, we established that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The browser will not render while a macrotask is running nor while microtasks are draining.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead, rendering only happens at stable boundaries. But this creates a new problem: If rendering only happens at specific boundaries, how do we run code &lt;strong&gt;just before&lt;/strong&gt; a render? If we want smooth animation, frame-aligned updates, or visual state that reflects the latest input, we need something that runs once per frame right before the browser renders. &lt;/p&gt;

&lt;p&gt;That is the scheduling gap that &lt;code&gt;requestAnimationFrame&lt;/code&gt; fills.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running the Experiments
&lt;/h2&gt;

&lt;p&gt;These experiments rely on the browser’s rendering behaviour.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a simple HTML file with the following content:
&lt;/li&gt;
&lt;/ol&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;id=&lt;/span&gt;&lt;span class="s"&gt;"box"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Initial&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Open the file in your browser&lt;/li&gt;
&lt;li&gt;You can run all code snippets in this series by pasting them into the browser console.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These examples will not work in Node.js because they depend on the DOM and browser rendering.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem Before &lt;code&gt;requestAnimationFrame&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Developers originally faced a challenge: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do I animate smoothly without freezing the UI?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We may run a naive loop like this where we want &lt;code&gt;update()&lt;/code&gt; to advance state and &lt;code&gt;render()&lt;/code&gt;  to mutate the DOM or canvas:&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;gameRunning&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;update&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;render&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;box&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;box&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Updated at &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;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLocaleTimeString&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="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;gameRunning&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;render&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;This will freeze your browser. Be prepared to close the browser tab after running this. &lt;/p&gt;

&lt;p&gt;This code completely blocks rendering. Since the call stack never empties, the browser never regains control and no rendering can occur. The page never updates.  &lt;/p&gt;

&lt;p&gt;So developers sliced work into smaller chunks to allow for breathing space for the browser to render:&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;update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;update&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;render&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;box&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;box&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Updated at &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;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLocaleTimeString&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="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;render&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="nx"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Use setTimeout() to chunk work&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this doesn't freeze the browser, be prepared to refresh the browser tab after running this. &lt;/p&gt;

&lt;p&gt;Now, the page renders the updates (the time shown on the page updates) to the &lt;code&gt;box&lt;/code&gt; content. Since most screens refresh at 60 Hz, 16 ms (1000 ms / 60 Hz) seemed like the right delay. This allowed the stack to clear between iterations so that the browser could render. &lt;/p&gt;

&lt;p&gt;But this was still guesswork.&lt;/p&gt;

&lt;p&gt;But this approach was not without its problems. First, timers are minimum delays, not guarantees. The callback may run for 20ms, 30ms or later.  Also, if the callback took longer than 16ms, we would miss frames and accumulate jitter and drop frames. Consequently, the callback may run before or after the render.&lt;/p&gt;

&lt;p&gt;Fundamentally, rendering is framed-based while timers are time-based, and therefore do not know when the browser is about to render. &lt;/p&gt;




&lt;h2&gt;
  
  
  Enter &lt;code&gt;requestAnimationFrame&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt; solves exactly this problem:&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;update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;update&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;render&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;box&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;box&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Updated at &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLocaleTimeString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also doesn't freeze the browser but be prepared to refresh the browser tab after running this. &lt;/p&gt;

&lt;p&gt;As before, the page renders the updates on the page. However, unlike timers, this runs before rendering. This sounds great but where exactly does it fit in the event loop model? Let's find out. &lt;/p&gt;




&lt;h2&gt;
  
  
  Test 1: Does &lt;code&gt;requestAnimationFrame&lt;/code&gt; Cut Ahead of Microtasks?
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;requestAnimationFrame&lt;/code&gt; runs just before rendering, it must not violate our previous rules:&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="nb"&gt;Promise&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="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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;microtask&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="nf"&gt;requestAnimationFrame&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;raf&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;The output in the console would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;microtask
raf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As established, the browser does not render while microtasks are pending. Microtasks still run first and &lt;code&gt;requestAnimationFrame&lt;/code&gt; does not change that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test 2: Is &lt;code&gt;requestAnimationFrame&lt;/code&gt; Just Another Task?
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;requestAnimationFrame&lt;/code&gt; were simply another macrotask, it would behave like &lt;code&gt;setTimeout&lt;/code&gt; and follow the ordering of tasks:&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;start&lt;/span&gt;&lt;span class="dl"&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;requestAnimationFrame&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;raf&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;end&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;In practice, you will often see the following in the console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;start
end
timeout
raf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, you may also see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;start
end
raf
timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ordering is not guaranteed. Even if your environment consistently shows one ordering, the key point is that the model does not enforce it. The browser is allowed to process another task first, or perform a render before continuing with tasks. Because of this, there is no fixed ordering between &lt;code&gt;setTimeout&lt;/code&gt; and &lt;code&gt;requestAnimationFrame&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This might seem surprising. If &lt;code&gt;requestAnimationFrame&lt;/code&gt; were just another macrotask, we would expect it to follow a consistent ordering relative to &lt;code&gt;setTimeout&lt;/code&gt;. But it doesn’t, suggesting that &lt;code&gt;requestAnimationFrame&lt;/code&gt; is not part of the task queue at all. Instead, it runs during the browser’s rendering phase, which is scheduled separately from tasks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test 3: Do Microtasks Inside &lt;code&gt;requestAnimationFrame&lt;/code&gt; Run Before Paint?
&lt;/h2&gt;

&lt;p&gt;What if &lt;code&gt;requestAnimationFrame&lt;/code&gt; also created microtasks?&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;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;box&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;box&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Frame start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nb"&gt;Promise&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="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="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Microtask update&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The typical output is for the page to show "Microtask update"&lt;/p&gt;

&lt;p&gt;When the &lt;code&gt;requestAnimationFrame&lt;/code&gt; callback runs, the DOM updates and a microtask is queued. The &lt;code&gt;requestAnimationFrame&lt;/code&gt; callback completes and microtasks drain. Only then can rendering occur.&lt;/p&gt;

&lt;p&gt;Even inside &lt;code&gt;requestAnimationFrame&lt;/code&gt;, microtasks must drain before rendering.&lt;/p&gt;




&lt;h2&gt;
  
  
  What &lt;code&gt;requestAnimationFrame&lt;/code&gt; Actually Guarantees
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt; guarantees that the callback runs before the browser's rendering. It runs at most once per frame and is aligned to the display's actual refresh rate, regardless of whether it is 60Hz or 120Hz. It pauses automatically in background tabs and skips frames when the browser is busy.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Complete Ordering
&lt;/h2&gt;

&lt;p&gt;We can now state the model: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Macrotask&lt;/li&gt;
&lt;li&gt;Drain microtasks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;requestAnimationFrame&lt;/code&gt; callbacks&lt;/li&gt;
&lt;li&gt;Drain microtasks&lt;/li&gt;
&lt;li&gt;Render&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the complete scheduling turn. Rendering is not part of task queue but is gated by it. &lt;code&gt;requestAnimationFrame&lt;/code&gt; is the only public API designed to hook into that pre-render phase. &lt;/p&gt;




&lt;h2&gt;
  
  
  What This Prepares Us For Next
&lt;/h2&gt;

&lt;p&gt;Now that we understand this structure, what do we do with it?&lt;/p&gt;

&lt;p&gt;In the final article of this series, we move from mechanism to consequence: &lt;/p&gt;

&lt;p&gt;What happens when a macrotask runs too long?&lt;br&gt;
What happens when microtasks never stop?&lt;br&gt;
Why does the UI freeze?&lt;br&gt;
Why are some updates never visible?&lt;br&gt;
Why do we sometimes need cleanup guards?&lt;/p&gt;

&lt;p&gt;One we understand who gets to run and when, we can reason about performance, responsiveness and architectural trade-offs with precision. &lt;/p&gt;

&lt;p&gt;This is where we go &lt;a href="https://dev.to/marshateo/the-scheduling-boundaries-behind-responsive-ui-2m56"&gt;next&lt;/a&gt;. &lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on my &lt;a href="https://www.marshateo.com/writing/requestAnimationFrame" rel="noopener noreferrer"&gt;website&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Thought Dark Mode Was a 30-Minute Task. It Turned Into a Full Refactor</title>
      <dc:creator>Marsha Teo</dc:creator>
      <pubDate>Wed, 06 May 2026 11:24:11 +0000</pubDate>
      <link>https://forem.com/marshateo/i-thought-dark-mode-was-a-30-minute-task-it-turned-into-a-full-refactor-5bk5</link>
      <guid>https://forem.com/marshateo/i-thought-dark-mode-was-a-30-minute-task-it-turned-into-a-full-refactor-5bk5</guid>
      <description>&lt;p&gt;I thought adding dark mode would take 30 minutes. It broke my website. &lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 1: Hardcoded colours
&lt;/h2&gt;

&lt;p&gt;I didn’t even use that many colours. How hard could it be? Turns out, very. &lt;/p&gt;

&lt;p&gt;My colours were hardcoded directly into Tailwind utility classes. A heading had a specific hex value, and a paragraph had another. To change the theme, I would have had to update each of these individually. No, thank you. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I introduced CSS variables and defined colours by role, not value. &lt;/p&gt;

&lt;p&gt;My first attempt had variables like &lt;code&gt;hover-dark&lt;/code&gt; and &lt;code&gt;hover-darker&lt;/code&gt;. That worked until I tried to invert the theme for dark mode. What would &lt;code&gt;hover-darker&lt;/code&gt; even mean in dark mode? &lt;/p&gt;

&lt;p&gt;Instead of asking &lt;em&gt;"What color should this element be?"&lt;/em&gt;, I had to ask &lt;em&gt;"What role does this color play?"&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;So I switched to variable names that were semantic, rather than literal: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--text-primary&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--text-secondary&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--background&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--border&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this, a heading wasn’t “black” anymore. It was &lt;code&gt;text-primary&lt;/code&gt;.  A background wasn’t “white”. It was &lt;code&gt;background-primary&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At this point, I thought I was mostly done. I wasn’t even close.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 2: Dark mode is not white on black
&lt;/h2&gt;

&lt;p&gt;I thought dark mode meant white text on a black background. It looked terrible. Who would've thought that having white text on black would feel so... bright? It was harsh, hard to read, and everything started blending together - almost like I had suddenly developed astigmatism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It turns out dark mode isn't really black and white. It's shades of grey. Instead of white, I used a light grey and text was miraculously legible again. For secondary text, &lt;em&gt;even lighter&lt;/em&gt; grey worked perfectly. &lt;/p&gt;

&lt;p&gt;Pushing contrast to the extreme with white text on black backgrounds was a disaster. Tuning that contrast and making it proportionate and layered made things way more comfortable. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4j2xau2cchfeab3qu4mc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4j2xau2cchfeab3qu4mc.png" alt="White text on black background" width="800" height="435"&gt;&lt;/a&gt;&lt;br&gt;White text on black was too much contrast on my screen. I felt it in my eyes.
  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft2d5fux8sv407x242mvf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft2d5fux8sv407x242mvf.png" alt="Grey text on black background" width="800" height="432"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
    Subtle change to reduce contrast, making it easier to read over a longer period of time&lt;br&gt;
  
  &lt;/p&gt;

&lt;p&gt;And that's all my problems solved, said no one ever. &lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 3: Everything breaks differently
&lt;/h2&gt;

&lt;p&gt;Even after fixing colours, things still looked off. Different parts of the UI broke in different ways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem 3.1: Typography (Tailwind)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tailwind’s typography plugin (&lt;code&gt;prose&lt;/code&gt;) worked great in light mode. But once I introduced my own variables, the defaults started conflicting with my system. Some styles updated, others didn’t. Fixing one thing broke another. The abstraction broke down, and the complexity I’d tried to hide came rushing back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I explicitly mapped Tailwind’s typography variables to my own. Instead of relying on defaults, I treated typography as part of my system.&lt;/p&gt;

&lt;p&gt;Once everything pointed back to the same set of variables, things became predictable again.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Problem 3.2: Code Syntax Highlighting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I use a lot of code snippets, especially in my &lt;a href="https://www.marshateo.com/writing/javascript-event-loop-landing" rel="noopener noreferrer"&gt;JavaScript event loop article series&lt;/a&gt;. For syntax highlighting, I had used Github's light theme for my light mode. Too bad it was impossible to read in dark mode. So I switched to Github Dark CSS - only for it to look off in light mode. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a while, I assumed I had to pick one. Eventually, I realised the obvious solution: use both Github and Github Dark CSS and switch dynamically based on the mode. It sounds pretty obvious now, but at the time, I genuinely thought I had to choose. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqwqznrxliul3e3us1ogo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqwqznrxliul3e3us1ogo.png" alt="Github CSS in Dark Mode" width="800" height="208"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
    Github's light theme in dark mode was impossible to read &lt;br&gt;
  
  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd99rq64p0ghfmpqvje29.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd99rq64p0ghfmpqvje29.png" alt="Github Dark CSS in Light Mode" width="800" height="205"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
    Github's dark theme in light mode looked washed out &lt;br&gt;
  
  &lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Problem 3.3: Images&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Images introduced a different kind of problem. Some worked fine. Others didn’t translate at all.&lt;/p&gt;

&lt;p&gt;My hero image is of a sunrise. From the start, I imagined using a sunset version for dark mode. Did I create dark mode just so that I can use this image? Maybe.&lt;/p&gt;

&lt;p&gt;Thankfully, this was easily implemented by including both images and switching between them based on the mode.&lt;/p&gt;

&lt;p&gt;But my SVG diagrams were harder. I tried making their colors dynamic using CSS variables but it didn’t work reliably.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of forcing everything to be dynamic, I created two versions of each diagram, one for each mode. It felt less elegant at first, but it worked better. Not everything should be dynamically styled.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs6nm78eeahkxcm44u5f0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs6nm78eeahkxcm44u5f0.gif" alt="SVG Diagrams Not Adjusted for Dark Mode" width="800" height="639"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
    Diagrams designed for light mode don’t translate automatically.&lt;br&gt;
  
  &lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 4: The flash
&lt;/h2&gt;

&lt;p&gt;After fixing everything, I refreshed the page. A flash of light mode appeared before it switched to dark. It was subtle, but definitely there. And yes, the temptation to pretend that didn't happen was definitely there too. &lt;/p&gt;

&lt;p&gt;The browser was rendering before the correct theme was applied. By the time JavaScript the correct theme was set, the browser had already painted the wrong one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff81fjwr8reqdtxhv3q6b.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff81fjwr8reqdtxhv3q6b.gif" alt="Flash of light mode when refreshing in dark mode" width="720" height="710"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
    The flash: light mode renders before dark mode is applied&lt;br&gt;
  
  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The theme needed to be determined before rendering. Moving the theme logic earlier removed the flash entirely. It was a small change technically, but it had a big impact on how the site felt.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real lesson
&lt;/h2&gt;

&lt;p&gt;I thought I was adding a feature: a toggle button and a visual enhancement that sits on top of everything else.&lt;/p&gt;

&lt;p&gt;But dark mode didn’t sit on top of my UI. It ran through it and every part of the system had to agree. None of the above was individually difficult. But together, they revealed that dark mode was a system and one that needs to be designed intentionally.&lt;/p&gt;




&lt;h2&gt;
  
  
  If you’re implementing dark mode
&lt;/h2&gt;

&lt;p&gt;A few things I wish I knew earlier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define colors by role, not value&lt;/li&gt;
&lt;li&gt;Avoid extreme contrast&lt;/li&gt;
&lt;li&gt;Treat typography as part of your system&lt;/li&gt;
&lt;li&gt;Don’t force everything to be dynamic&lt;/li&gt;
&lt;li&gt;Handle theme selection before render&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you’re curious, the full implementation and visuals are on my &lt;a href="https://www.marshateo.com" rel="noopener noreferrer"&gt;site&lt;/a&gt;. This article was also originally published &lt;a href="https://www.marshateo.com/writing/portfolio-dark-mode" rel="noopener noreferrer"&gt;there&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Rendering Is a Browser Decision, Not a JavaScript One</title>
      <dc:creator>Marsha Teo</dc:creator>
      <pubDate>Thu, 30 Apr 2026 17:37:14 +0000</pubDate>
      <link>https://forem.com/marshateo/rendering-is-a-browser-decision-not-a-javascript-one-47e3</link>
      <guid>https://forem.com/marshateo/rendering-is-a-browser-decision-not-a-javascript-one-47e3</guid>
      <description>&lt;p&gt;This is the fifth article in a series on how JavaScript actually runs. You can read the full series &lt;a href="https://dev.to/marshateo/javascript-event-loop-series-building-the-event-loop-mental-model-from-experiments-4d8i"&gt;here&lt;/a&gt; or on my &lt;a href="https://www.marshateo.com/writing/javascript-event-loop-landing" rel="noopener noreferrer"&gt;website&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;You change the DOM.&lt;/p&gt;

&lt;p&gt;You expect the screen to update.&lt;/p&gt;

&lt;p&gt;It doesn’t.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;In the earlier articles, we established three constraints:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;JavaScript runs to completion.&lt;/li&gt;
&lt;li&gt;Tasks form scheduling boundaries.&lt;/li&gt;
&lt;li&gt;Microtasks must fully drain before moving on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now we add a fourth:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The browser will not render while a macrotask is running nor while microtasks are draining.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Rendering Is a Browser Decision
&lt;/h2&gt;

&lt;p&gt;Up to this point in the series, we’ve focused on two pieces of the system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The JavaScript engine, which executes code and manages the call stack.&lt;/li&gt;
&lt;li&gt;The runtime, which provides the event loop and scheduling rules.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But neither of these is responsible for rendering.&lt;/p&gt;

&lt;p&gt;Beyond the JavaScript engine and the runtime, the browser also contains a rendering engine — the subsystem responsible for layout and painting.&lt;/p&gt;

&lt;p&gt;The engine executes your code. The runtime manages when that code runs. The rendering engine decides when the result becomes visible.&lt;/p&gt;

&lt;p&gt;For simplicity, this article will refer to that rendering engine simply as the browser.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Rendering Misconception
&lt;/h2&gt;

&lt;p&gt;When I first started learning JavaScript, I carried several mental models that felt reasonable: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DOM updates render immediately&lt;/li&gt;
&lt;li&gt;If I change the UI, the user will see it right away.&lt;/li&gt;
&lt;li&gt;The browser renders continuously at 60fps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These felt natural because the screen often updates quickly. But they're incomplete: Rendering does not happen whenever the DOM changes. Instead, rendering happens only when there is a 'safe opportunity',  after the current macrotask finishes and the microtask queue is empty. &lt;/p&gt;

&lt;p&gt;Rendering is not triggered by DOM mutation. It is gated by scheduling boundaries. Let’s test that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running the Experiments
&lt;/h2&gt;

&lt;p&gt;These experiments rely on the browser’s rendering behaviour.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a simple HTML file with the following content:
&lt;/li&gt;
&lt;/ol&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;id=&lt;/span&gt;&lt;span class="s"&gt;"box"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Initial&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Open the file in your browser&lt;/li&gt;
&lt;li&gt;You can run all code snippets in this series by pasting them into the browser console.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These examples will not work in Node.js because they depend on the DOM and browser rendering.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test 1: DOM Updates Inside One Macrotask
&lt;/h2&gt;

&lt;p&gt;What happens when we have multiple DOM updates within the same macrotask? We may write something like the following, using a placeholder before the final string is ready:&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;box&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;box&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Temporary string&lt;/span&gt;&lt;span class="dl"&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;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;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e9&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="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Final string of Test 1&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;We might worry that &lt;code&gt;"Temporary string"&lt;/code&gt; would briefly appear before &lt;code&gt;"Final string"&lt;/code&gt; is ready. But that doesn't happen. Phew!&lt;/p&gt;

&lt;p&gt;Both updates occur inside the same macrotask and the browser refuses to render mid-task. It waits until the entire macrotask is finished, checks that there is no microtask in the queue and finally considers rendering. &lt;/p&gt;

&lt;p&gt;The intermediate DOM states never show. &lt;/p&gt;




&lt;h2&gt;
  
  
  Test 2: Microtasks Also Delay Rendering
&lt;/h2&gt;

&lt;p&gt;What if the second update happens in a microtask instead? Would  &lt;code&gt;"Temporary string"&lt;/code&gt; appear briefly?&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;box&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;box&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Temporary string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nb"&gt;Promise&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="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="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Final string of Test 2&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;Again, we only see &lt;code&gt;"Final string of Test 2"&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;The initial macrotask runs and sets &lt;code&gt;"Temporary string"&lt;/code&gt;. After the call stack is empty, the microtask runs immediately after to update the DOM to &lt;code&gt;"Final string"&lt;/code&gt;. Only now does the browser get an opportunity to render. &lt;/p&gt;

&lt;p&gt;Microtasks delay rendering just like synchronous code does. &lt;/p&gt;




&lt;h2&gt;
  
  
  Test 3: Breaking Into a New Task Allows Paint
&lt;/h2&gt;

&lt;p&gt;Now consider a timer callback:&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;box&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;box&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Temporary string&lt;/span&gt;&lt;span class="dl"&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Final string of Test 3&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="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time we may see &lt;code&gt;"Temporary string"&lt;/code&gt;, followed by &lt;code&gt;"Final string of Test 3"&lt;/code&gt; a second later.&lt;/p&gt;

&lt;p&gt;Unlike the previous tests, we have now introduced a task boundary. The browser finishes the initial macrotask, drains microtasks (there are none here) and gets an opportunity to render. If it chooses to render, &lt;code&gt;"Temporary string"&lt;/code&gt; becomes visible. &lt;/p&gt;

&lt;p&gt;Later, when the runtime schedules the timer's macrotask, the DOM updates to &lt;code&gt;"Final string"&lt;/code&gt; and the next render will reflect this. &lt;/p&gt;

&lt;p&gt;Rendering is allowed at task boundaries. This does not mean that rendering is guaranteed between macrotasks; only that it can only happen there. &lt;/p&gt;




&lt;h2&gt;
  
  
  Why Rendering Waits
&lt;/h2&gt;

&lt;p&gt;If the browser could render in the middle of a macrotask or in the middle of microtask draining, it could display half-updated DOM, inconsistent layout and/or partially computed state. &lt;/p&gt;

&lt;p&gt;Thankfully, with this constraint, the browser renders only stable states, where a macrotask has finished and the microtask queue is empty. There would be no partial work in progress and hence rendering is atomic with respect to JavaScript execution. &lt;/p&gt;




&lt;h2&gt;
  
  
  The Correct Mental Model
&lt;/h2&gt;

&lt;p&gt;With these tests, we've shown that the browser does not render whenever the DOM changes. Instead:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The browser renders only after JavaScript finishes its turn.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here, a "turn" means the current macrotask completes and the microtask queue has been fully drained. &lt;/p&gt;

&lt;p&gt;Rendering is allowed only at those boundaries. This does not mean the browser renders after every turn, only that it cannot render during one. The rendering decision is gated by the same scheduling rules we’ve been building throughout this series.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Prepares Us For Next
&lt;/h2&gt;

&lt;p&gt;If rendering only happens at specific boundaries, a new question emerges: How do we write code that runs at the right moment?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;setTimeout&lt;/code&gt; creates a new macrotask but it does not align with the browser's frame timing. Microtasks delay rendering but they do not schedule it. If we want smooth animation and responsive updates, we need a way to run code just before the browser renders the next frame. &lt;/p&gt;

&lt;p&gt;This is what &lt;code&gt;requestAnimationFrame&lt;/code&gt; is design for. In the &lt;a href="https://dev.to/marshateo/requestanimationframe-the-missing-scheduling-layer-1el0"&gt;next article&lt;/a&gt;, we'll look more closely at how the browser's rendering cycle works and how to schedule work in harmony with it. &lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on my &lt;a href="https://www.marshateo.com/writing/rendering" rel="noopener noreferrer"&gt;website&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>async / await: Pausing a Function Without Pausing JavaScript</title>
      <dc:creator>Marsha Teo</dc:creator>
      <pubDate>Mon, 27 Apr 2026 06:35:28 +0000</pubDate>
      <link>https://forem.com/marshateo/async-await-pausing-a-function-without-pausing-javascript-3c0e</link>
      <guid>https://forem.com/marshateo/async-await-pausing-a-function-without-pausing-javascript-3c0e</guid>
      <description>&lt;p&gt;&lt;em&gt;This is the fourth article in a series on how JavaScript actually runs. You can read the full series &lt;a href="//../README.md"&gt;here&lt;/a&gt; or on my &lt;a href="https://www.marshateo.com/writing/javascript-event-loop-landing" rel="noopener noreferrer"&gt;website&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;In the &lt;a href="https://dev.to/marshateo/microtasks-why-promises-run-first-4ba1"&gt;last article&lt;/a&gt;, we established a precise rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Once a macrotask finishes, JavaScript drains all microtasks before selecting the next macrotask.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Microtasks like Promises are continuations that must complete before the runtime moves on to the next macrotask. &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; is often described as syntactic sugar over Promises. If that's the case, we should already understand how &lt;code&gt;await&lt;/code&gt; works. &lt;/p&gt;

&lt;p&gt;Let’s see.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Possible Mental Models
&lt;/h2&gt;

&lt;p&gt;Before we look at any code, consider this question: When JavaScript reaches &lt;code&gt;await&lt;/code&gt;, what actually happens? &lt;/p&gt;

&lt;p&gt;It's surprisingly easy to carry one of the following mental models (I certainly did when I first started learning JavaScript). Which of these feel right?&lt;/p&gt;

&lt;p&gt;a. &lt;code&gt;await&lt;/code&gt; blocks the entire program before the value resolves, like &lt;code&gt;sleep()&lt;/code&gt; in C or C++ &lt;br&gt;
b. &lt;code&gt;await&lt;/code&gt; pauses the function and immediately yields to the event loop, creating a new macrotask &lt;br&gt;
c. &lt;code&gt;await&lt;/code&gt; splits the function and schedules the remainder as a microtask &lt;/p&gt;

&lt;p&gt;Pause for a moment. Pick one and we'll test it. &lt;/p&gt;
&lt;h2&gt;
  
  
  Running the Experiments
&lt;/h2&gt;

&lt;p&gt;You can run all code snippets in this series by pasting them into the browser console.&lt;/p&gt;

&lt;p&gt;While some examples work in Node.js, others rely on browser APIs (like rendering or &lt;code&gt;requestAnimationFrame&lt;/code&gt;), so the browser is the most reliable environment.&lt;/p&gt;
&lt;h2&gt;
  
  
  Test for Mental Model A (&lt;code&gt;await&lt;/code&gt; Blocks)
&lt;/h2&gt;

&lt;p&gt;Let's see if &lt;code&gt;await&lt;/code&gt; pauses the entire program:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Inside test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;After await&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Before test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;After test&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;If &lt;code&gt;await&lt;/code&gt; blocked the program, we would expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before test
Inside test
After await
After test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, we observe that &lt;code&gt;After test&lt;/code&gt; runs before &lt;code&gt;After await&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before test
Inside test
After test
After await
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It appears that &lt;code&gt;await&lt;/code&gt; does not block JavaScript. Execution continues after calling &lt;code&gt;test()&lt;/code&gt;. So whatever &lt;code&gt;await&lt;/code&gt; does, it does not stop everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test for Mental Model B (&lt;code&gt;await&lt;/code&gt; Yields)
&lt;/h2&gt;

&lt;p&gt;Perhaps &lt;code&gt;await&lt;/code&gt; pauses the function and immediately hands control back to the runtime for it to choose another macrotask:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Inside test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;After await&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="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Before test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;After test&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;If &lt;code&gt;await&lt;/code&gt; yielded control to the runtime and created a macrotask boundary, the timer might run first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before test
Inside test
After test
timeout
After await
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But it never does. We instead observe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before test
Inside test
After test
After await
timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The continuation after &lt;code&gt;await&lt;/code&gt; always runs before the timer. This matches the rule from the last article:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Microtasks are drained before any macrotask is selected.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;await&lt;/code&gt; does not create a macrotask.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mental Model C: The Only Survivor
&lt;/h2&gt;

&lt;p&gt;So far, the only model consistent with every test is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When execution reaches &lt;code&gt;await&lt;/code&gt;, the function pauses and the rest of the function is queued as a microtask.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When JavaScript reaches &lt;code&gt;await value&lt;/code&gt;, the engine conceptually performs something like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Convert value into a promise if it isn’t one already.&lt;/li&gt;
&lt;li&gt;Wrap the rest of the function in a continuation.&lt;/li&gt;
&lt;li&gt;Schedule the continuation as a microtask.&lt;/li&gt;
&lt;li&gt;Return immediately to the caller.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The function simply splits at this point. This happens even if the value is already resolved:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Inside test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;After await&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Before test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;After test&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;We still observe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before test
Inside test
After test
After await
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though &lt;code&gt;42&lt;/code&gt; is not a promise, the remainder of the function runs later. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;await&lt;/code&gt; will also split the function however many times it appears in the function. Consider:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Inside test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;After first await&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;After second await&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Before test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;After test&lt;/span&gt;&lt;span class="dl"&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout&lt;/span&gt;&lt;span class="dl"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Observed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before test
Inside test
After test
After first await
After second await
timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;await&lt;/code&gt; causes the function to split with each &lt;code&gt;await&lt;/code&gt; creating a new continuation that runs as a microtask. Both continuations run before the timer since the runtime drains microtasks fully before scheduling the next macrotask. &lt;/p&gt;

&lt;p&gt;That split results in a pause in the current &lt;code&gt;async&lt;/code&gt; function. There is no other pause - not in the call stack, the event loop nor the entire program. &lt;/p&gt;

&lt;p&gt;With &lt;code&gt;await&lt;/code&gt;, control immediately returns to the caller. That’s why this works:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;done&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;loadData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;continue&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;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;start
continue
done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function pauses and the program continues.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; As Syntactic Sugar Over Promises
&lt;/h2&gt;

&lt;p&gt;You may have heard that &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; is syntactic sugar over promises. That’s true, but only if we are precise what the sugar expands into. At its core, &lt;code&gt;await&lt;/code&gt; is equivalent to calling &lt;code&gt;.then()&lt;/code&gt; but with one addition. &lt;/p&gt;

&lt;p&gt;When JavaScript reaches &lt;code&gt;await value&lt;/code&gt;, it registers a continuation like &lt;code&gt;.then()&lt;/code&gt; would but it also splits the current function at that point and schedules the remainder to run later as a microtask. &lt;/p&gt;

&lt;p&gt;With raw &lt;code&gt;.then()&lt;/code&gt;, you manually place the continuation inside a callback. With &lt;code&gt;await&lt;/code&gt;, the language automatically pauses the function, preserves its local variables and control flow and resumes it later in the microtask queue. It is a function split backed by the microtask system. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;await&lt;/code&gt; is &lt;code&gt;.then()&lt;/code&gt; plus structured function splitting and microtask resumption. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What About &lt;code&gt;async&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;So far, we’ve focused entirely on &lt;code&gt;await&lt;/code&gt;. But every example also had &lt;code&gt;async&lt;/code&gt;. If &lt;code&gt;await&lt;/code&gt; is responsible for splitting the function, what does &lt;code&gt;async&lt;/code&gt; actually do? Let’s see.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;test&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="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&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;p1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Without await:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p1&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;p2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;With await:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Without await: Promise { 42 }
With await: 42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;main()&lt;/code&gt; function runs synchronously until the first &lt;code&gt;await&lt;/code&gt;. When &lt;code&gt;test()&lt;/code&gt; is called without &lt;code&gt;await&lt;/code&gt;, there is no pause and no microtask. The body of &lt;code&gt;test()&lt;/code&gt; runs immediately. The only difference is that &lt;code&gt;test()&lt;/code&gt; returns a &lt;code&gt;Promise&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;async&lt;/code&gt; on its own does not introduce asynchronous work. Instead, it changes the return type of the function: &lt;code&gt;test()&lt;/code&gt; returns a &lt;code&gt;Promise&lt;/code&gt;, even if it completes synchronously.  &lt;/p&gt;

&lt;p&gt;When &lt;code&gt;test()&lt;/code&gt; is called with &lt;code&gt;await&lt;/code&gt;, something different happens. The call to &lt;code&gt;test()&lt;/code&gt; still runs immediately, and it still returns a &lt;code&gt;Promise&lt;/code&gt;. But now &lt;code&gt;main()&lt;/code&gt; pauses at the &lt;code&gt;await&lt;/code&gt;. The remainder of &lt;code&gt;main()&lt;/code&gt; is wrapped into a continuation and scheduled as a microtask. When that microtask runs, the &lt;code&gt;Promise&lt;/code&gt; returned by &lt;code&gt;test()&lt;/code&gt; is unwrapped and its resolved value becomes the value of the &lt;code&gt;await&lt;/code&gt; expression. &lt;/p&gt;

&lt;p&gt;Without &lt;code&gt;await&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt; is a &lt;code&gt;Promise&lt;/code&gt;. With &lt;code&gt;await&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt; is the resolved value of that &lt;code&gt;Promise&lt;/code&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Correct Mental Model
&lt;/h2&gt;

&lt;p&gt;We can now separate the two keywords clearly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;async&lt;/code&gt; changes what the function returns.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;await&lt;/code&gt; changes how the function executes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If there is no &lt;code&gt;await&lt;/code&gt;, an &lt;code&gt;async&lt;/code&gt; function can run entirely synchronously. If there is an &lt;code&gt;await&lt;/code&gt;, the function splits and resumes as a microtask continuation.&lt;/p&gt;

&lt;p&gt;Once you see this, &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; stops being mysterious. It becomes a thin layer over the microtask system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Prepares Us For Next
&lt;/h2&gt;

&lt;p&gt;We now understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Macrotasks are chosen one at a time.&lt;/li&gt;
&lt;li&gt;Microtasks drain completely.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Promise&lt;/code&gt; callbacks are continuations.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;await&lt;/code&gt; creates microtask continuations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is one more piece missing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When does rendering happen?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To answer that we need to look beyond JavaScript execution and into the browser's frame lifecycle. That is the next layer of the event loop, and that's where we go &lt;a href="https://dev.to/marshateo/rendering-is-a-browser-decision-not-a-javascript-one-47e3"&gt;next&lt;/a&gt;. &lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on my &lt;a href="https://www.marshateo.com/writing/async-await" rel="noopener noreferrer"&gt;website&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Microtasks: Why Promises Run First</title>
      <dc:creator>Marsha Teo</dc:creator>
      <pubDate>Thu, 23 Apr 2026 08:10:16 +0000</pubDate>
      <link>https://forem.com/marshateo/microtasks-why-promises-run-first-4ba1</link>
      <guid>https://forem.com/marshateo/microtasks-why-promises-run-first-4ba1</guid>
      <description>&lt;p&gt;&lt;em&gt;This is the third article in a series on how JavaScript actually runs. You can read the full series &lt;a href="https://dev.to/marshateo/javascript-event-loop-series-building-the-event-loop-mental-model-from-experiments-4d8i"&gt;here&lt;/a&gt; or on my &lt;a href="https://www.marshateo.com/writing/javascript-event-loop-landing" rel="noopener noreferrer"&gt;website&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;In the &lt;a href="https://dev.to/marshateo/macrotasks-what-a-task-actually-is-4pbd"&gt;last article&lt;/a&gt;, we established that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;JavaScript execution cannot be interrupted.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once a macrotask starts, nothing cuts in. Only after it completes does the runtime select the next macrotask from the queue. &lt;/p&gt;

&lt;p&gt;But consider 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;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout&lt;/span&gt;&lt;span class="dl"&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="nb"&gt;Promise&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="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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;promise&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync done&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;The output is always:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sync done
promise
timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;setTimeout&lt;/code&gt; and &lt;code&gt;Promise.then&lt;/code&gt; are asynchronous and both schedule work to run later. If macrotasks are chosen one at a time, and nothing interrupts them, then promises should behave like timers. But that's not the case. The promise runs first, every time. Why?&lt;/p&gt;

&lt;p&gt;If our macrotask model of JavaScript were complete, this ordering would not be guaranteed. Something else must exist. Specifically, there is another category of work in JavaScript: &lt;strong&gt;microtasks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;They do not interrupt the current macrotask. And yet they run before the runtime selects the next macrotask. Before we define them fully, we should understand why such a mechanism is needed. &lt;/p&gt;




&lt;h2&gt;
  
  
  The Tempting but Incomplete Explanation
&lt;/h2&gt;

&lt;p&gt;Many explanations jump immediately to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Promises use the microtask queue, which runs before the macrotask queue.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That statement is technically correct. But it explains nothing. Why are there two queues? Why does one outrank the other?&lt;/p&gt;

&lt;p&gt;If we stop here, microtasks feel arbitrary. Let's instead find out more.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running the Experiments
&lt;/h2&gt;

&lt;p&gt;You can run all code snippets in this series by pasting them into the browser console.&lt;/p&gt;

&lt;p&gt;While some examples work in Node.js, others rely on browser APIs (like rendering or &lt;code&gt;requestAnimationFrame&lt;/code&gt;), so the browser is the most reliable environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Hypothesis: Promises Are Just Higher-Priority Tasks
&lt;/h2&gt;

&lt;p&gt;A reasonable mental model is that microtasks are just higher-priority tasks. When we have timers and promises, timers go into one queue, promises go into another, and the promise queue is checked first.&lt;/p&gt;

&lt;p&gt;Let's test 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;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout&lt;/span&gt;&lt;span class="dl"&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="nb"&gt;Promise&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="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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;promise 1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;Promise&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="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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;promise 2&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync done&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;If promises are merely higher-priority tasks, we may expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sync done
promise 1
timeout
promise 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After &lt;code&gt;sync done&lt;/code&gt;, the runtime has at least two pending pieces of work: the timer callback and the first promise callback. Since promise callbacks have higher priority, the runtime chooses the promise first. Consequently, &lt;code&gt;promise 1&lt;/code&gt; runs before &lt;code&gt;timeout&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;promise 1&lt;/code&gt; runs, it schedules another promise callback: &lt;code&gt;promise 2&lt;/code&gt;. At this point, the runtime could choose between the existing timer callback or the newly scheduled promise callback. If promise callbacks were just higher priority macrotasks, the runtime should be free to interleave them.&lt;/p&gt;

&lt;p&gt;However, the actual output is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sync done
promise 1
promise 2
timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;promise 2&lt;/code&gt; runs immediately after &lt;code&gt;promise 1&lt;/code&gt;, before the &lt;code&gt;timeout&lt;/code&gt; is even considered. &lt;/p&gt;




&lt;h2&gt;
  
  
  The Rule That Must Exist
&lt;/h2&gt;

&lt;p&gt;The only model consistent with this behavior is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Once microtask execution begins, all microtasks must run to completion before the runtime considers another macrotask.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Promise callbacks are not independent tasks competing with timers. They are unfinished work from the current turn of execution. They are continuations and continuations must complete before control is returned to the runtime. &lt;/p&gt;




&lt;h2&gt;
  
  
  Reframing Microtasks Properly
&lt;/h2&gt;

&lt;p&gt;A microtask is not a faster callback nor is it a convenience queue. &lt;code&gt;Promise&lt;/code&gt; callbacks are the most common example of microtasks, but this mechanism also underlie &lt;code&gt;async&lt;/code&gt; functions and &lt;code&gt;MutationObserver&lt;/code&gt; callbacks. Broadly, a microtask is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Work that must be completed before JavaScript yields control back to the runtime.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;microtasks run after the current macrotask finishes,&lt;/li&gt;
&lt;li&gt;microtasks run before the runtime chooses another macrotask,&lt;/li&gt;
&lt;li&gt;the runtime drains the microtask queue completely,&lt;/li&gt;
&lt;li&gt;microtasks can schedule more microtasks,&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They exist to preserve atomicity across asynchronous boundaries. &lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Rule Must Exist
&lt;/h2&gt;

&lt;p&gt;If microtasks were treated like ordinary macrotasks, promise chains could interleave with unrelated work. That would introduce subtle inconsistencies and expose partially completed state. &lt;/p&gt;

&lt;p&gt;Consider 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;let&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;data&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="nb"&gt;Promise&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="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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;result&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loading&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This callback represents a single logical transition where the data arrives and loading ends. From the programmer's perspective, these two assignments belong together. &lt;/p&gt;

&lt;p&gt;If the runtime were allowed to pause this callback midway or run unrelated macrotasks before it completes, external code could observe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ loading: true, data: "result" }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a partially completed update (data has arrived but loading is still &lt;code&gt;true&lt;/code&gt;). JavaScript avoids this by enforcing: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Once the current macrotask finishes, the runtime runs all microtasks run before selecting another macrotask.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This ensures that promises are continuations of the current turn of execution. And these continuations must complete before control returns to the runtime. That guarantee makes promise chains predictable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Promise.resolve()
  .then(() =&amp;gt; step1())
  .then(() =&amp;gt; step2());
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first &lt;code&gt;.then()&lt;/code&gt; callback is queued as a microtask. After the promise returned by &lt;code&gt;step1()&lt;/code&gt; settles, the second callback is queued. A promise chain schedules its continuations incrementally, not all at once. &lt;/p&gt;

&lt;p&gt;Yet because the runtime must drain the microtask queue completely before selecting another macrotask, these incrementally scheduled callbacks still run back-to-back, without unrelated timers or events cutting in between them. The continuation may be deferred but it is never fragmented. &lt;/p&gt;




&lt;h2&gt;
  
  
  The Draining Behavior
&lt;/h2&gt;

&lt;p&gt;Microtasks are not executed one-by-one with runtime checks between them. They are drained in a loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;while (microtask queue is not empty) {
  run next microtask
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is why nested promises run immediately. That is why infinite promise loops freeze the page. Consider:&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;loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;Promise&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loop&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout fired&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run this, be prepared to close the page, since this experiment creates an infinite microtask loop.&lt;/p&gt;

&lt;p&gt;The page would freeze and &lt;code&gt;timeout fired&lt;/code&gt; is never logged since a new microtask is queued every time &lt;code&gt;loop&lt;/code&gt; is called. The runtime is not allowed to proceed to another macrotask while microtasks remain. Microtasks are not candidates for task selection. They are executed automatically as part of finishing the current turn.&lt;/p&gt;




&lt;h2&gt;
  
  
  The JavaScript Turn Model
&lt;/h2&gt;

&lt;p&gt;We can now describe a single turn of JavaScript execution:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The runtime chooses a macrotask.&lt;/li&gt;
&lt;li&gt;JavaScript executes synchronously.&lt;/li&gt;
&lt;li&gt;Once the call stack is empty, the runtime drains the microtask queue.&lt;/li&gt;
&lt;li&gt;Only then can the runtime consider another macrotask.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the event loop from JavaScript's perspective. In later articles, we will extend this model to include rendering and the browser's frame lifecycle. loop. &lt;/p&gt;




&lt;h2&gt;
  
  
  The Mental Model to Keep
&lt;/h2&gt;

&lt;p&gt;When debugging async behavior, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did we just finish a macrotask?&lt;/li&gt;
&lt;li&gt;Are there microtasks pending?&lt;/li&gt;
&lt;li&gt;Has the runtime been allowed to choose another macrotask yet?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If microtasks exist, the answer is always:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No, the runtime must wait.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What This Prepares Us For Next
&lt;/h2&gt;

&lt;p&gt;If microtasks are mandatory continuations, then what exactly does &lt;code&gt;await&lt;/code&gt; do?&lt;/p&gt;

&lt;p&gt;Does it pause execution?&lt;br&gt;
Does it create a new task?&lt;br&gt;
Or does it quietly hook into this same microtask mechanism?&lt;/p&gt;

&lt;p&gt;Understanding that requires looking at &lt;code&gt;async&lt;/code&gt; functions more closely.&lt;/p&gt;

&lt;p&gt;That is the subject of the &lt;a href="https://dev.to/marshateo/async-await-pausing-a-function-without-pausing-javascript-3c0e"&gt;next article&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on my &lt;a href="https://www.marshateo.com/writing/microtasks" rel="noopener noreferrer"&gt;website&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Macrotasks: What a Task Actually Is</title>
      <dc:creator>Marsha Teo</dc:creator>
      <pubDate>Fri, 17 Apr 2026 13:48:14 +0000</pubDate>
      <link>https://forem.com/marshateo/macrotasks-what-a-task-actually-is-4pbd</link>
      <guid>https://forem.com/marshateo/macrotasks-what-a-task-actually-is-4pbd</guid>
      <description>&lt;p&gt;&lt;em&gt;This is the second article in a series on how JavaScript actually runs. You can read the full series &lt;a href="https://dev.to/marshateo/javascript-event-loop-series-building-the-event-loop-mental-model-from-experiments-4d8i"&gt;here&lt;/a&gt; or on my &lt;a href="https://www.marshateo.com/writing/javascript-event-loop-landing" rel="noopener noreferrer"&gt;website&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;In the &lt;a href="https://dev.to/marshateo/the-javascript-runtime-fixing-the-mental-model-5f5b"&gt;previous article&lt;/a&gt;, we established that&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;JavaScript executes synchronously inside a task, and nothing can interrupt that execution.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This explains why timers don't cut in, why promises wait and why loops block everything else. But if JavaScript runs 'inside a task', where does that task begin and end? Is the entire script one task? Is each function call its own task? &lt;/p&gt;

&lt;p&gt;This article exists to answer more precisely:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What exactly is a task?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Wrong Mental Model of a Task
&lt;/h2&gt;

&lt;p&gt;You would think that 'task' refers to a unit of work — something with a duration, a beginning, and an end - and imagine that in JavaScript, a 'task' refers to a big chunk of execution. Maybe each statement is its own task; Each function call creates a new task; Control-flow disruptions using &lt;code&gt;try&lt;/code&gt;/&lt;code&gt;catch&lt;/code&gt; create task boundaries where the runtime is allowed to pause the current execution to run something else. With this, synchronous code consists of smaller tasks, and  execution could be subdivided internally. &lt;/p&gt;

&lt;p&gt;Let's test that idea. &lt;/p&gt;




&lt;h2&gt;
  
  
  Running the Experiments
&lt;/h2&gt;

&lt;p&gt;You can run all code snippets in this series by pasting them into the browser console.&lt;/p&gt;

&lt;p&gt;While some examples work in Node.js, others rely on browser APIs (like rendering or &lt;code&gt;requestAnimationFrame&lt;/code&gt;), so the browser is the most reliable environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test 1: The Initial Script Is One Task
&lt;/h2&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/marshateo/the-javascript-runtime-fixing-the-mental-model-5f5b"&gt;previous article&lt;/a&gt;, we tested whether &lt;code&gt;setTimeout&lt;/code&gt; could interrupt synchronous execution:&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Schedule asynchronous callback with setTimeout&lt;/span&gt;
&lt;span class="nf"&gt;setTimeout&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout fired&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="mi"&gt;0&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;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;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e9&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync end&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;What we always observed was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sync start
sync end
timeout fired
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The entire global script executed as one uninterrupted block. The &lt;code&gt;setTimeout&lt;/code&gt; callback ran later in a separate execution window. There are 2 tasks here: the initial script and the timeout callback. &lt;/p&gt;

&lt;p&gt;Even though the timer expired quickly, the callback did not execute immediately. Instead, when the timer expired, the callback became eligible to run and waited in the queue managed by the runtime. &lt;/p&gt;




&lt;h2&gt;
  
  
  Test 2: Internal Structure Does Not Create Task Boundaries
&lt;/h2&gt;

&lt;p&gt;Could each function call be its own 'task' internally? If function calls created new tasks, the runtime could switch between them.&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;a&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&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;function&lt;/span&gt; &lt;span class="nf"&gt;b&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;b&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;c&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;c&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;c&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="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;a&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If function calls created new tasks, we might expect the timeout to run between &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, or &lt;code&gt;c&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But the output is always:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a
b
c
timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though the call stack grows and shrinks, JavaScript remains inside the same task the entire time. Function calls change the call stack but do not create new tasks because these changes to the internal call stack are invisible to the runtime. The runtime only observes whether the call stack is empty.&lt;/p&gt;

&lt;p&gt;This is the case even in deep recursion cases:&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;start&lt;/span&gt;&lt;span class="dl"&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout&lt;/span&gt;&lt;span class="dl"&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;recurse&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;recurse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;recurse&lt;/span&gt;&lt;span class="p"&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;end&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;The runtime does not care how long the call stack is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;start
end
timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Test 3: Exceptions Do Not Create Task Boundaries
&lt;/h2&gt;

&lt;p&gt;Perhaps abrupt control flow — like exceptions — creates a task  boundary. Maybe now execution 'breaks' enough that the runtime gets a chance to run something else.&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;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout&lt;/span&gt;&lt;span class="dl"&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;before throw&lt;/span&gt;&lt;span class="dl"&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="s2"&gt;boom&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;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;caught error&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;after catch&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;The output is always:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before throw
caught error
after catch
timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though execution jumps abruptly from &lt;code&gt;throw&lt;/code&gt; to &lt;code&gt;catch&lt;/code&gt;, it never leaves the current task. JavaScript remains in the same task until &lt;code&gt;after catch&lt;/code&gt; and the runtime schedules the timer callback task. &lt;/p&gt;

&lt;p&gt;Now let's remove the &lt;code&gt;try&lt;/code&gt;/&lt;code&gt;catch&lt;/code&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="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout task&lt;/span&gt;&lt;span class="dl"&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;a&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a: enter&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;b&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a: finally (ran during unwind)&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a: after b (never)&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;b&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;b: enter&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;c&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;b: finally (ran during unwind)&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;c&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;c: throw&lt;/span&gt;&lt;span class="dl"&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="s2"&gt;boom&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="nf"&gt;a&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;global: after a (never)&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;What happens?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a: enter
b: enter
c: throw
b: finally (ran during unwind)
a: finally (ran during unwind)
Uncaught Error: boom
timeout task
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Control never returns to &lt;code&gt;a: after b&lt;/code&gt; or &lt;code&gt;global: after a&lt;/code&gt; after the exception was thrown. The current task terminates immediately when the error escapes the call stack. As the stack unwinds, the &lt;code&gt;finally&lt;/code&gt; blocks run. Only after the unwind is complete does the runtime regain control and select select the next task. An uncaught exception ends the current task. It does not subdivide it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test 4: User Events Do Not Interrupt
&lt;/h2&gt;

&lt;p&gt;Now let's introduce an external event: a user click.&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click handler ran&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;start long task&lt;/span&gt;&lt;span class="dl"&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;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;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e9&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;end long task&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;If you click the page while the loop is running, what happens?&lt;/p&gt;

&lt;p&gt;The page will appear frozen for a few seconds (the loop typically takes a couple of seconds to complete on most machines). The click is detected instantly by the browser but the click handler does not run. Only after the loop finishes do you see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;start long task
end long task
click handler ran
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The click created a new task: the runtime (the browser) captured the click event and scheduled the task. That task waits for the engine to become idle. &lt;/p&gt;




&lt;h2&gt;
  
  
  Reframing Tasks Correctly: Permission, Not Duration
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;A task describes why JavaScript is allowed to start running at all.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The runtime (the browser or Node.js) observes events outside the JavaScript engine. These events include timers expiring and user interactions. Each of these produces a request to run JavaScript. A task is that request. &lt;/p&gt;

&lt;p&gt;A task does not describe how long JavaScript runs. It describes an entry point into execution. For instance, the delay passed to &lt;code&gt;setTimeout&lt;/code&gt; is a minimum delay, not a guaranteed execution time. When the timer expires, the callback becomes eligible to run. It does not execute immediately. It must still wait for the current task to finish and for the call stack to become empty. &lt;/p&gt;

&lt;p&gt;When the call stack is empty, the runtime chooses one task and hands control to the engine. The JavaScript engine then runs that task synchronously to completion. &lt;/p&gt;

&lt;p&gt;From the runtime’s point of view, the engine is either running or idle. An empty call stack is the only signal that matters. The runtime is free to wait, listen, and prepare callbacks — but it is not free to execute them whenever it likes. It must wait until JavaScript stops.&lt;/p&gt;

&lt;p&gt;Tasks are a coordination mechanism between the runtime and the engine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why These Are Called "Macrotasks"
&lt;/h2&gt;

&lt;p&gt;The kind of task we have been discussing has a more precise name: &lt;strong&gt;macrotask&lt;/strong&gt;. Examples include the initial script execution, a &lt;code&gt;setTimeout&lt;/code&gt; callback and a user event handler. &lt;/p&gt;

&lt;p&gt;"Macro" does not mean large nor long-running. It distinguishes these tasks from another scheduling mechanisms we will introduce in the next article.&lt;/p&gt;

&lt;p&gt;For now, a macrotask is: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;a request from runtime that allows the engine to begin executing code. &lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Runtime Model
&lt;/h2&gt;

&lt;p&gt;At this point, we can describe the system precisely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The runtime collects requests to run JavaScript.&lt;/li&gt;
&lt;li&gt;Each request is a macrotask.&lt;/li&gt;
&lt;li&gt;When the call stack is empty, the runtime selects a macrotask.&lt;/li&gt;
&lt;li&gt;JavaScript runs synchronously to completion.&lt;/li&gt;
&lt;li&gt;Only then can another macrotask be considered.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the runtime's perspective, the only thing that matters is whether the call stack is empty. &lt;/p&gt;

&lt;p&gt;This explains why task boundaries are coarse. Task boundaries do not occur between statements, function calls, loop iterations, recursive calls nor try/catch blocks. They occur at large structural entry points like an entire script, an entire event handler, or an entire timer callback. The runtime does not see internal structure; It only sees whether execution has finished.  &lt;/p&gt;




&lt;h2&gt;
  
  
  Where We Go Next
&lt;/h2&gt;

&lt;p&gt;At this point, we know that a macrotask runs to completion. But what happens when, inside a macrotask, the code schedules more work that logically belongs to the same operation?&lt;/p&gt;

&lt;p&gt;Should it interrupt the current macrotask? (But we just established that this is not possible).&lt;/p&gt;

&lt;p&gt;Should it wait behind the other tasks in the queue? (But this would delay it unpredictably.)&lt;/p&gt;

&lt;p&gt;Neither is ideal. Instead, JavaScript has a mechanism that does not interrupt the current macrotask but runs &lt;strong&gt;before&lt;/strong&gt; the runtime selects the next macrotask. This mechanism is &lt;strong&gt;microtasks&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is where we go &lt;a href="https://dev.to/marshateo/microtasks-why-promises-run-first-4ba1"&gt;next&lt;/a&gt;. &lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on my &lt;a href="https://www.marshateo.com/writing/macrotasks" rel="noopener noreferrer"&gt;website&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The JavaScript Runtime: Fixing the Mental Model</title>
      <dc:creator>Marsha Teo</dc:creator>
      <pubDate>Fri, 10 Apr 2026 20:36:36 +0000</pubDate>
      <link>https://forem.com/marshateo/the-javascript-runtime-fixing-the-mental-model-5f5b</link>
      <guid>https://forem.com/marshateo/the-javascript-runtime-fixing-the-mental-model-5f5b</guid>
      <description>&lt;p&gt;&lt;em&gt;This is the first article in a series on how JavaScript actually runs. You can read the full series &lt;a href="https://dev.to/marshateo/javascript-event-loop-series-building-the-event-loop-mental-model-from-experiments-4d8i"&gt;here&lt;/a&gt; or on my &lt;a href="https://www.marshateo.com/writing/javascript-event-loop-landing" rel="noopener noreferrer"&gt;website&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Most explanations of JavaScript's event loop start with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;JavaScript is single-threaded.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This statement is technically true but doesn't explain certain behaviors in JavaScript you may have noticed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;setTimeout&lt;/code&gt; doesn't interrupt loops after the timer has run out&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setTimeout&lt;/code&gt; doesn't block (like &lt;code&gt;sleep(1)&lt;/code&gt; in C)&lt;/li&gt;
&lt;li&gt;A resolved &lt;code&gt;Promise&lt;/code&gt; still runs after synchronous code&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;await&lt;/code&gt; pauses a function but doesn't freeze the page&lt;/li&gt;
&lt;li&gt;Rendering sometimes wait&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So we're left asking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why didn’t my timeout fire?&lt;/li&gt;
&lt;li&gt;Why didn’t it interrupt my loop?&lt;/li&gt;
&lt;li&gt;Why does &lt;code&gt;await&lt;/code&gt; pause &lt;em&gt;this&lt;/em&gt; but not &lt;em&gt;everything&lt;/em&gt;?&lt;/li&gt;
&lt;li&gt;Why does nothing ever 'cut in'?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this series, we'll build the understanding needed to make JavaScript stop feeling magical. &lt;/p&gt;

&lt;p&gt;Today's core claim is simple: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;JavaScript executes synchronously inside a task, and nothing can interrupt that execution.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What Does "Synchronous" and "Asynchronous" Really Mean?
&lt;/h2&gt;

&lt;p&gt;First, let's define 'synchronous' and 'asynchronous' precisely.&lt;/p&gt;

&lt;p&gt;Synchronous execution refers to code that runs immediately, executing from top to bottom via the call stack. We will show that this cannot be interrupted in JavaScript. &lt;/p&gt;

&lt;p&gt;By contrast, asynchronous code refers to code whose result is not available immediately. Some work is initiated now, but its continuation runs later. In practice, this almost always involves a &lt;strong&gt;callback&lt;/strong&gt; - a function that is scheduled to execute in the future. In JavaScript, asynchronous code includes not only &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; but also other mechanisms like &lt;code&gt;setTimeout&lt;/code&gt;, &lt;code&gt;Promise&lt;/code&gt;, and &lt;code&gt;requestAnimationFrame&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;As this series will make clear, asynchronous mechanisms do not block nor interrupt the call stack. Instead, they arrange for something to run later via scheduling.&lt;/p&gt;

&lt;p&gt;For now, let's test the claim directly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can asynchronous callbacks interrupt synchronous execution?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Running the Experiments
&lt;/h2&gt;

&lt;p&gt;You can run all code snippets in this series by pasting them into the browser console.&lt;/p&gt;

&lt;p&gt;While some examples work in Node.js, others rely on browser APIs (like rendering or &lt;code&gt;requestAnimationFrame&lt;/code&gt;), so the browser is the most reliable environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Baseline Case: Pure Synchronous Execution
&lt;/h2&gt;

&lt;p&gt;Let's start with a simple &lt;code&gt;for&lt;/code&gt; loop:&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync start&lt;/span&gt;&lt;span class="dl"&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;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;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e9&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync end&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;The code runs from top to bottom. The output is unsurprisingly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sync start
sync end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Test 1: Can &lt;code&gt;setTimeout&lt;/code&gt; Interrupt a Loop?
&lt;/h2&gt;

&lt;p&gt;Let's introduce an asynchronous mechanism with &lt;code&gt;setTimeout&lt;/code&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Schedule asynchronous callback with setTimeout&lt;/span&gt;
&lt;span class="nf"&gt;setTimeout&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout fired&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="mi"&gt;0&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;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;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e9&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync end&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;If &lt;code&gt;setTimeout&lt;/code&gt; could interrupt, we would see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sync start
timeout fired
sync end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But what actually happens is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sync start
sync end
timeout fired
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The timer did not 'cut in': It waited until the loop finished running. &lt;/p&gt;




&lt;h2&gt;
  
  
  Test 2: Can Promises Interrupt?
&lt;/h2&gt;

&lt;p&gt;Now let's try a &lt;code&gt;Promise&lt;/code&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Schedule asynchronous callback with a Promise&lt;/span&gt;
&lt;span class="nb"&gt;Promise&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="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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;promise callback&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;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;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;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e9&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync end&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;If Promises could interrupt execution, we would see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sync start
promise callback
sync end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the observed behavior is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sync start
sync end
promise callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even a resolved &lt;code&gt;Promise&lt;/code&gt; does not interrupt synchronous execution. Once JavaScript starts running, it runs to completion. &lt;/p&gt;




&lt;h2&gt;
  
  
  Destroying the Wrong Mental Models
&lt;/h2&gt;

&lt;p&gt;It's easy to imagine JavaScript like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;JavaScript is running, but timers or promises can interrupt it when they're ready&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words, &lt;code&gt;setTimeout&lt;/code&gt; fires when ready and &lt;code&gt;Promise&lt;/code&gt; callbacks can cut in once resolved. &lt;/p&gt;

&lt;p&gt;This resembles signal handlers in C where a signal can interrupt a running program, jump to the handler function and then resume execution afterward. That is pre-emptive behavior where execution can be paused at arbitrary instruction boundaries and control temporarily transferred elsewhere. JavaScript does not behave this way.&lt;/p&gt;

&lt;p&gt;We may also hold the mental model that: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Timers or promises run JavaScript on another thread.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this model, JavaScript runs concurrently in multiple threads: The script is running on one thread; Timers run in a background thread; Promises resolve in another (or the same) background thread. If that were true, we would see interleaved console output.  &lt;/p&gt;

&lt;p&gt;However, in both tests, there is no interruption nor interleaved output between &lt;code&gt;sync start&lt;/code&gt; and &lt;code&gt;sync end&lt;/code&gt; in the console. These tests force us to accept that JavaScript execution is not pre-emptive. When JavaScript starts executing a task, it continues until the call stack is empty. Only then can anything else run JavaScript. Asynchronous work waits and queues.&lt;/p&gt;




&lt;h2&gt;
  
  
  But Where Does Asynchronous Work Wait?
&lt;/h2&gt;

&lt;p&gt;JavaScript's behavior makes more sense when you realize that when you run JavaScript in the browser, you are not just running a language. You are running two cooperating systems: the JavaScript &lt;strong&gt;Engine&lt;/strong&gt; and the JavaScript &lt;strong&gt;Runtime Environment&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;The engine (V8 in Chrome/Node, SpiderMonkey in Firefox, JavaScriptCore in Safari) parses and compiles code, manages memory and executes functions via the call stack. When we say "JavaScript runs", we are referring to the engine. It knows variables, functions and the call stack. It does &lt;strong&gt;not&lt;/strong&gt; know timers, the DOM, HTTP requests nor rendering. &lt;/p&gt;

&lt;p&gt;Everything else is handled by the runtime (the browser or Node). For instance, the runtime provides web APIs (&lt;code&gt;setTimeout&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt;, DOM events). Importantly, it also performs the asynchronous work &lt;em&gt;outside&lt;/em&gt; the engine and decides when JavaScript is allowed to run again. This is the missing link: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The engine executes. The runtime schedules. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let's revisit our first experiment:&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync start&lt;/span&gt;&lt;span class="dl"&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout fired&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="mi"&gt;0&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;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;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e9&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync end&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;While the loop is running, what is happening structurally?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────────────────┐  ┌───────────────────────────────┐
│         CALL STACK            │  │         TASK QUEUE            │
├───────────────────────────────┤  ├───────────────────────────────┤
│ for loop                      │  │ timeout callback (waiting)    │
│ global script                 │  |                               | 
└───────────────────────────────┘  └───────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;for&lt;/code&gt; loop occupies the call stack. Even after the timer has expired, its callback doesn't interrupt the call stack. Instead, it waits in a queue managed by the runtime. The engine cannot run it (nor anything new) because the call stack is not empty. More broadly, the runtime schedules what the engine executes while the engine just executes whatever gets placed on the stack. &lt;/p&gt;




&lt;h2&gt;
  
  
  Introducing Tasks
&lt;/h2&gt;

&lt;p&gt;When the runtime grants permission for the engine to execute JavaScript, that moment is an entry point into execution. This entry point or permission to run JavaScript is called a &lt;strong&gt;task&lt;/strong&gt;. Examples include the initial script execution and the &lt;code&gt;setTimeout&lt;/code&gt; callback. Once a task starts, JavaScript runs synchronously. Asynchronous mechanisms do not interrupt a task but instead schedules future tasks. &lt;/p&gt;

&lt;p&gt;No worries if this doesn't make complete sense yet. We will refine "task" in later articles. For now, this is enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Sets Up
&lt;/h2&gt;

&lt;p&gt;From this article, we have established:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript runs synchronously inside a task&lt;/li&gt;
&lt;li&gt;Nothing can interrupt that execution&lt;/li&gt;
&lt;li&gt;Asynchronous mechanisms do not cut in - they schedule. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But if asynchronous callbacks don't interrupt running code, how and when are they allowed to run? What exactly is a 'task'? Where are these queues? Who decides what runs next?&lt;/p&gt;

&lt;p&gt;This is the subject of the &lt;a href="https://dev.to/marshateo/macrotasks-what-a-task-actually-is-4pbd"&gt;next article&lt;/a&gt;. &lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on my &lt;a href="https://www.marshateo.com/writing/runtime-mental-model" rel="noopener noreferrer"&gt;website&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>JavaScript Event Loop Series: Building the Event Loop Mental Model from Experiments</title>
      <dc:creator>Marsha Teo</dc:creator>
      <pubDate>Fri, 10 Apr 2026 20:35:43 +0000</pubDate>
      <link>https://forem.com/marshateo/javascript-event-loop-series-building-the-event-loop-mental-model-from-experiments-4d8i</link>
      <guid>https://forem.com/marshateo/javascript-event-loop-series-building-the-event-loop-mental-model-from-experiments-4d8i</guid>
      <description>&lt;p&gt;I wrote this series because JavaScript has many "asynchronous" mechanisms (&lt;code&gt;await&lt;/code&gt;, &lt;code&gt;setTimeout&lt;/code&gt;, &lt;code&gt;Promise&lt;/code&gt;, &lt;code&gt;requestAnimationFrame&lt;/code&gt;) that look similar but behave very differently.&lt;/p&gt;

&lt;p&gt;At first, I assumed they were interchangeable but that assumption quickly broke when I started debugging: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why &lt;code&gt;setTimeout(..., 0)&lt;/code&gt; doesn’t "run immediately"&lt;/li&gt;
&lt;li&gt;Why &lt;code&gt;await&lt;/code&gt; pauses a function but doesn’t freeze the page&lt;/li&gt;
&lt;li&gt;Why DOM updates sometimes don’t show up when you expect&lt;/li&gt;
&lt;li&gt;Why some "async" code still blocks rendering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These behaviours only make sense with the right mental model. This series builds that model by experimenting with small code snippets. &lt;/p&gt;

&lt;p&gt;The core idea is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;JavaScript runs to completion inside a task, and nothing interrupts it. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From there, we layer in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;macrotasks&lt;/strong&gt; (what a task actually is),&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;microtasks&lt;/strong&gt; (why Promises run first), &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt;&lt;/strong&gt; (pausing a function without pausing JavaScript), &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;rendering&lt;/strong&gt; (why the screen doesn’t update mid-turn), &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt;&lt;/strong&gt; (the missing pre-render scheduling layer), and finally, &lt;/li&gt;
&lt;li&gt;what this means for &lt;strong&gt;real UI code&lt;/strong&gt;. &lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;This series is for you if you’ve ever felt that JavaScript async behavior is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;predictable in practice, but unclear in theory
&lt;/li&gt;
&lt;li&gt;"working" … until it suddenly doesn’t
&lt;/li&gt;
&lt;li&gt;full of rules you remember, but don’t fully understand
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don’t need prior knowledge of the event loop. The goal is to build a mental model you can use to reason about behavior — not just memorize it.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to read this series
&lt;/h2&gt;

&lt;p&gt;Each article builds on the previous one. You &lt;em&gt;can&lt;/em&gt; jump around, but the payoff is highest if you go in order.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you want the &lt;strong&gt;core mental model quickly&lt;/strong&gt;: read Articles 1–3
&lt;/li&gt;
&lt;li&gt;If you care about &lt;strong&gt;rendering and UI behavior&lt;/strong&gt;: Articles 5–7 connect the model to what you see on screen
&lt;/li&gt;
&lt;li&gt;If you just want answers: each article is self-contained, but the full model only emerges across the series&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also read this series on my &lt;a href="https://www.marshateo.com/writing/javascript-event-loop-landing" rel="noopener noreferrer"&gt;website&lt;/a&gt; (recommended for the best reading experience).&lt;/p&gt;




&lt;h2&gt;
  
  
  The articles
&lt;/h2&gt;

&lt;p&gt;Here’s how the model unfolds:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1) The JavaScript Runtime: Fixing the Mental Model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why doesn’t &lt;code&gt;setTimeout&lt;/code&gt; interrupt your code? This article breaks the illusion: JavaScript runs synchronously, and async APIs don’t interrupt. Instead, they schedule.&lt;/p&gt;

&lt;p&gt;Read it &lt;a href="https://dev.to/marshateo/the-javascript-runtime-fixing-the-mental-model-5f5b"&gt;here&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2) Macrotasks: What a Task Actually Is&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If nothing can interrupt JavaScript, when does anything else run? This article reframes tasks as entry points into execution, not chunks of work.&lt;/p&gt;

&lt;p&gt;Read it &lt;a href="https://dev.to/marshateo/macrotasks-what-a-task-actually-is-4pbd"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3) Microtasks: Why Promises Run First&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why do Promises always run before &lt;code&gt;setTimeout&lt;/code&gt;? This article reveals microtasks as mandatory continuations that must run before JavaScript moves on.&lt;/p&gt;

&lt;p&gt;Read it &lt;a href="https://dev.to/marshateo/microtasks-why-promises-run-first-4ba1"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4) &lt;code&gt;async&lt;/code&gt; / &lt;code&gt;await&lt;/code&gt;: Pausing Functions Without Pausing the World&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Does &lt;code&gt;await&lt;/code&gt; pause your program or just your function? This article shows how &lt;code&gt;await&lt;/code&gt; actually works.&lt;/p&gt;

&lt;p&gt;Read it &lt;a href="https://dev.to/marshateo/async-await-pausing-a-function-without-pausing-javascript-3c0e"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5) Rendering Is a Browser Decision, Not a JavaScript One&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You updated the DOM. So why didn’t the screen change? This article explains why rendering is not triggered by JavaScript, but gated by it.&lt;/p&gt;

&lt;p&gt;Read it &lt;a href="https://dev.to/marshateo/rendering-is-a-browser-decision-not-a-javascript-one-47e3"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6) &lt;code&gt;requestAnimationFrame&lt;/code&gt;: The Missing Scheduling Layer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If rendering only happens at certain moments, how do you run code at the right time? This article introduces &lt;code&gt;requestAnimationFrame&lt;/code&gt; as the missing scheduling layer.&lt;/p&gt;

&lt;p&gt;Read it &lt;a href="https://dev.to/marshateo/requestanimationframe-the-missing-scheduling-layer-1el0"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7) The Scheduling Boundaries Behind Responsive UI&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why do UIs freeze, skip updates, or feel laggy? This article connects the event loop to real-world UI behavior and shows how to work with the browser, not against it.&lt;/p&gt;

&lt;p&gt;Read it &lt;a href="https://dev.to/marshateo/the-scheduling-boundaries-behind-responsive-ui-2m56"&gt;here&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;This is the model everything in this series builds toward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The browser (runtime) starts a macrotask
&lt;/li&gt;
&lt;li&gt;JavaScript runs synchronously until the call stack is empty
&lt;/li&gt;
&lt;li&gt;The runtime drains all microtasks
&lt;/li&gt;
&lt;li&gt;The browser runs any &lt;code&gt;requestAnimationFrame&lt;/code&gt; callbacks
&lt;/li&gt;
&lt;li&gt;Microtasks drain again (if any were queued during &lt;code&gt;requestAnimationFrame&lt;/code&gt;)
&lt;/li&gt;
&lt;li&gt;Only then can rendering happen
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Continue the Conversation
&lt;/h2&gt;

&lt;p&gt;This series was originally written and is maintained on my &lt;a href="https://www.marshateo.com/writing/javascript-event-loop-landing" rel="noopener noreferrer"&gt;website&lt;/a&gt;. If you prefer a single place with all updates and future articles, you can follow along there.&lt;/p&gt;

&lt;p&gt;If you want to discuss edge cases, counterexamples, or how this interacts with real applications, I’m always happy to chat.&lt;/p&gt;

&lt;p&gt;My personal website is &lt;a href="https://marshateo.com" rel="noopener noreferrer"&gt;https://marshateo.com&lt;/a&gt;.&lt;/p&gt;

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