๐Ÿ’ก Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:

    • Download Node.js to run JavaScript on your computer
    • Use your browser's Developer Console (Press F12) to test code snippets
    • Create a .html file with <script> tags and open it in your browser

    Advanced JavaScript โ€” Event Loop, Microtasks & Async Internals

    What You'll Learn in This Lesson

    • โœ“Call Stack & Task Queues
    • โœ“Microtasks vs Macrotasks priority
    • โœ“How Promises bypass setTimeout
    • โœ“Event Loop starvation bugs
    • โœ“Browser rendering pipeline
    • โœ“Node.js vs Browser differences

    ๐ŸŽช Real-World Analogy: Single Juggler at a Circus

    Imagine JavaScript as a single juggler who can only hold one ball at a time. When someone tosses them a new ball (like a network request), they hand it to an assistant (Web API) who holds it temporarily. When the assistant is done (request completes), they place the ball in a queue. The juggler only grabs from the queue when their hands are empty. This is why JavaScript is "non-blocking" โ€” the juggler never stops to wait; they keep juggling while assistants handle the waiting.

    ๐Ÿ“‹ Event Loop Priority Order:

    PriorityQueue TypeExamplesWhen Processed
    1stCall StackSynchronous codeImmediately
    2ndMicrotask QueuePromise.then, queueMicrotaskAfter current task, before render
    3rdMacrotask QueuesetTimeout, events, fetchAfter microtasks complete

    The JavaScript event loop is the hidden engine that powers asynchronous behaviour in browsers, Node.js, mobile runtimes, UI frameworks, and high-performance servers. Although JavaScript is single-threaded, it achieves concurrency using a sophisticated scheduling system built around the call stack, task queues, microtask queues, Web APIs, and the event loop cycle. Understanding this system is essential for writing non-blocking code, fixing race conditions, preventing UI freezes, and optimising back-end performance. Most bugs in async JavaScript come from not understanding when something executes โ€” not how it executes.

    The event loop is designed around one rule: JavaScript never pauses the main thread. Instead, long-running operations (network requests, timers, DOM events, I/O, file operations) run in the background through browser/Node APIs. Once finished, they push callbacks into queues, which the event loop processes in a strict order.

    ๐Ÿ”ฅ Call Stack โ€” The Execution Engine

    The call stack is a LIFO (Last In, First Out) structure. Each time a function executes, the engine creates a new execution context and pushes it onto the stack.

    Call Stack Example

    See how functions are pushed and popped from the call stack

    Try it Yourself ยป
    JavaScript
    function a() {
      b();
    }
    function b() {
      console.log("B");
    }
    a();

    Stack order: global() โ†’ a() โ†’ b() โ†’ console.log()

    When the stack is empty, the event loop may process queued callbacks. A blocked stack freezes everything โ€” this is why infinite loops freeze your browser.

    ๐Ÿ”ฅ Web APIs / Node APIs โ€” Async Work is Not Done by JavaScript

    JavaScript itself cannot do asynchronous work. Instead, browsers have Web APIs and Node.js has C++ bindings and libuv threadpool.

    Web APIs Example

    See how setTimeout delegates work to browser APIs

    Try it Yourself ยป
    JavaScript
    setTimeout(() => console.log("Done"), 1000);

    setTimeout does not wait inside JavaScript. It is delegated to browser timers. When done, the callback is pushed to the macrotask / task queue.

    ๐Ÿ”ฅ Macrotask Queue (Task Queue) โ€” Timers, I/O, Rendering

    Macrotasks (also called "tasks") include: setTimeout, setInterval, setImmediate (Node), requestAnimationFrame, DOM events, File I/O callbacks, Network callbacks, MessageChannel, Script parsing.

    Macrotask Queue Example

    See how setTimeout(0) still waits for the next loop cycle

    Try it Yourself ยป
    JavaScript
    console.log(1);
    setTimeout(() => console.log(2), 0);
    console.log(3);

    Output: 1 3 2. Even setTimeout(...,0) is never immediate โ€” it always waits for the next loop cycle.

    ๐Ÿ”ฅ Microtask Queue โ€” Higher Priority Than Everything Else

    Microtasks include: Promise callbacks (.then, .catch, .finally), MutationObserver, queueMicrotask(), async/await continuations, some V8 internal jobs.

    Microtasks run before any macrotasks โ€” and after each synchronous execution block.

    Microtask Queue Example

    See how Promise callbacks run before macrotasks

    Try it Yourself ยป
    JavaScript
    console.log("A");
    
    Promise.resolve().then(() => console.log("B"));
    
    console.log("C");

    Output: A C B. This is because microtasks run after the call stack empties but before any macrotasks.

    ๐Ÿ”ฅ Microtasks Can Starve the Event Loop

    Because microtasks run before macrotasks, infinite microtask scheduling blocks timers and event handlers:

    Microtask Starvation

    See how infinite microtasks can starve the event loop

    Try it Yourself ยป
    JavaScript
    function loop() {
        Promise.resolve().then(loop);
    }
    loop();
    
    setTimeout(() => console.log("Timer fired"), 0);

    The timer never fires. This is called microtask starvation and is a real source of production bugs.

    ๐Ÿ”ฅ Event Loop Order โ€” The Golden Rule

    Every cycle works like this:

    1. Execute everything in the call stack
    2. Execute the entire microtask queue (until empty)
    3. Render updates (browser only)
    4. Execute one macrotask
    5. Repeat forever

    This priority system means: Promises always beat setTimeout, await resolves before timers, UI updates may be delayed if microtasks are heavy.

    ๐Ÿ”ฅ Promises vs setTimeout โ€” Why Promises Always Win

    Promise vs setTimeout

    See why Promise callbacks always run before setTimeout

    Try it Yourself ยป
    JavaScript
    setTimeout(() => console.log("Timeout"), 0);
    
    Promise.resolve().then(() => console.log("Promise"));

    Output: Promise Timeout. Because Promise callbacks = microtasks, setTimeout = macrotask. Microtasks always run first.

    ๐Ÿ”ฅ Async/Await โ€” It's Just Syntactic Sugar Over Promises

    Async/Await Sugar

    See how await splits the function into two parts

    Try it Yourself ยป
    JavaScript
    async function test() {
        console.log(1);
        await null;
        console.log(2);
    }
    test();
    console.log(3);

    Output: 1 3 2. Because await splits function into two parts. The second part becomes a microtask.

    ๐Ÿ”ฅ Long Example: Understanding Full Async Flow

    Full Async Flow

    Understand the complete async execution order

    Try it Yourself ยป
    JavaScript
    console.log("start");
    
    setTimeout(() => console.log("timeout"), 0);
    
    Promise.resolve()
      .then(() => {
        console.log("promise1");
        return Promise.resolve();
      })
      .then(() => console.log("promise2"));
    
    console.log("end");

    Execution order:

    1. "start" โ€” sync
    2. "end" โ€” sync
    3. Microtask queue: "promise1"
    4. Microtask queue: "promise2"
    5. Macrotask: "timeout"

    Final output: start end promise1 promise2 timeout

    ๐Ÿ”ฅ The Major Async Queues of JavaScript

    1. Microtask Queue (Highest Priority)

    This queue instantly executes after every synchronous block. It contains: Promise reactions (then, catch, finally), queueMicrotask(), MutationObserver, Async/await continuation jobs.

    Microtasks can "starve" the event loop โ€” meaning they can block macrotasks indefinitely if scheduled too aggressively.

    queueMicrotask Starvation

    See how queueMicrotask can starve the event loop

    Try it Yourself ยป
    JavaScript
    function loop() {
      queueMicrotask(loop);
    }
    loop();
    
    setTimeout(() => console.log("Timeout never runs"), 0);

    2. Animation Frame Queue (Browser Only)

    This queue runs before the browser draws a new frame (ideally at 60fps). Used for: smooth animations, games, physics engines, canvas rendering, UI transitions.

    requestAnimationFrame

    See how rAF runs before the browser paints

    Try it Yourself ยป
    JavaScript
    requestAnimationFrame(() => {
      console.log("Runs before the next frame is painted");
    });

    3. Macrotask Queue (Lower Priority)

    This includes: Timers (setTimeout, setInterval), UI events, I/O callbacks, MessageChannel, Script execution, Network callbacks (XHR), setImmediate (Node.js).

    The event loop processes ONE macrotask per turn.

    ๐Ÿ”ฅ How Browsers Render Pages During the Event Loop

    The browser executes tasks in this order:

    1. Run JavaScript
    2. Run all microtasks
    3. Render UI changes (paint, style recalc, layout)
    4. Run the next macro task

    If microtasks run too long, the browser skips rendering cycles, causing: Lag, Frozen animations, Slow interactions, Delayed clicks, Janky scrolling.

    ๐Ÿ”ฅ How Node.js Differs From Browsers

    Node.js uses libuv, which introduces more granular queues: Next Tick Queue (highest priority in Node), Promise/Microtask Queue, Timers Phase, Pending Callbacks, Idle/Prepare, Poll Phase, Check Phase (setImmediate), Close Callbacks.

    Node.js vs Browser

    See how async ordering differs between environments

    Try it Yourself ยป
    JavaScript
    setTimeout(() => console.log("Timeout"), 0);
    setImmediate(() => console.log("Immediate"));
    
    Promise.resolve().then(() => console.log("Microtask"));

    Output order varies between browser and Node. Promises always run before both.

    ๐Ÿ”ฅ Async Generators, Streams & the Event Loop

    Async generators (async function*) create sequences of values pulled over time. Under the hood, each yield schedules a microtask before returning control to the caller.

    Async Generators

    See how async generators schedule microtasks between yields

    Try it Yourself ยป
    JavaScript
    async function* streamData() {
      yield "A";
      await new Promise(r => setTimeout(r, 0));
      yield "B";
      yield "C";
    }
    
    (async () => {
      for await (const v of streamData()) {
        console.log(v);
      }
    })();

    Output order: A B C. Microtasks schedule between each step. This model is used heavily in: live chat streams, video decoding pipelines, sensor feeds, AI inference batching, WebSocket data, game engines, backend log streams.

    ๐Ÿ”ฅ The Browser's Rendering Pipeline & The Event Loop

    Each frame (ideally 16.6ms per frame) follows:

    1. Handle user input
    2. Run JavaScript
    3. Run microtasks
    4. Recalculate styles
    5. Recalculate layout
    6. Update layers
    7. Paint
    8. Commit frame to screen

    If your JavaScript uses too many microtasks, the browser delays layout and painting. This causes: low FPS, stuttering animations, delayed click responses, broken transitions.

    ๐Ÿ”ฅ How requestIdleCallback() Helps

    This function runs code only when: the browser is idle, CPU load is low, no urgent tasks exist.

    requestIdleCallback

    See how to run code when the browser is idle

    Try it Yourself ยป
    JavaScript
    requestIdleCallback(() => {
      console.log("Browser is idle, running safe work");
    });

    Great for: preloading assets, warm caching, indexing content, analytics batching, preparing UI transitions.

    ๐Ÿ”ฅ Network, Fetch & the Event Loop

    Network requests (fetch) don't run inside JS. They run inside browser networking threads. When they finish, they schedule a macrotask.

    Fetch & Event Loop

    See how network requests interact with the event loop

    Try it Yourself ยป
    JavaScript
    console.log("Start");
    fetch("url").then(() => console.log("Fetch done"));
    console.log("End");

    Output: Start End Fetch done

    ๐Ÿ”ฅ Professional Strategy for Predictable Async Code

    1. Keep microtask chains short
    2. Offload heavy computation
    3. Prefer requestAnimationFrame for visual updates
    4. Prefer requestIdleCallback for non-urgent tasks
    5. Use Web Workers for anything CPU-heavy
    6. Log execution order when debugging
    7. Avoid deep Promise nesting
    8. Test on slow devices
    9. Remember Node and Browser ordering differ
    10. Never rely on timers for precise orchestration

    ๐Ÿ”ฅ Final Example โ€” Full Event Loop Ordering Breakdown

    Full Event Loop Ordering

    See the complete async system in action

    Try it Yourself ยป
    JavaScript
    console.log("Start");
    
    setTimeout(() => console.log("Macrotask"), 0);
    
    Promise.resolve()
      .then(() => console.log("Microtask 1"))
      .then(() => console.log("Microtask 2"));
    
    requestAnimationFrame(() => console.log("Animation Frame"));
    
    console.log("End");

    Expected output: Start End Microtask 1 Microtask 2 Animation Frame Macrotask

    This single example demonstrates the entire async system.

    ๐ŸŽฏ Key Takeaways

    • The event loop is the foundation of all async JavaScript behaviour
    • Microtasks always execute before macrotasks
    • Infinite microtasks can starve the event loop
    • Understanding async internals is essential for building scalable applications
    • Promises and async/await are syntactic sugar over the event loop
    • Browser rendering depends on proper async task scheduling

    Sign up for free to track which lessons you've completed and get learning reminders.

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy Policy โ€ข Terms of Service