Skillversitas

Event Loop

The Event Loop is the mechanism that lets JavaScript handle asynchronous operations even though JavaScript is single-threaded. The Event Loop continuously checks whether there are tasks to run and orchestrates their execution order.

Event Loop Components

   ┌──────────────────────────┐
   │       Call Stack          │ ← the function currently being executed
   └──────────────────────────┘
            ↕
   ┌──────────────────────────┐
   │      Web APIs / Node      │ ← setTimeout, fetch, DOM events, etc.
   └──────────────────────────┘
            ↓
   ┌──────────────────────────┐
   │     Microtask Queue      │ ← Promise.then, queueMicrotask, MutationObserver
   │   (prioritized)          │
   └──────────────────────────┘
            ↓
   ┌──────────────────────────┐
   │     Macrotask Queue      │ ← setTimeout, setInterval, I/O, UI rendering
   └──────────────────────────┘

The Call Stack

The Call Stack is the stack of functions currently being executed. JavaScript is single-threaded — only one function runs at a time.

function first() {
  console.log('first');
}

function second() {
  first();
  console.log('second');
}

function third() {
  second();
  console.log('third');
}

third();

// Call Stack:
// 1. third() enters the stack
// 2. second() enters the stack (from third)
// 3. first() enters the stack (from second)
// 4. first() leaves the stack → prints "first"
// 5. second() continues → prints "second"
// 6. second() leaves the stack
// 7. third() continues → prints "third"
// 8. third() leaves the stack → stack is empty

// Output: "first" → "second" → "third"

The Microtask Queue

The microtask queue is processed EVERY TIME the call stack becomes empty, before any macrotask. This is what makes Promise.then always run faster than setTimeout.

console.log('1: Start');

setTimeout(() => {
  console.log('2: setTimeout'); // macrotask
}, 0);

Promise.resolve().then(() => {
  console.log('3: Promise'); // microtask
});

console.log('4: End');

// Output:
// 1: Start
// 4: End
// 3: Promise  ← the microtask is processed first
// 2: setTimeout ← the macrotask is processed after the microtask completes

The Macrotask Queue

Macrotasks are processed after all microtasks are done. This includes setTimeout, setInterval, event handlers, and I/O callbacks.

console.log('Start');

setTimeout(() => {
  console.log('setTimeout 1');
}, 0);

setTimeout(() => {
  console.log('setTimeout 2');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise 1');
}).then(() => {
  console.log('Promise 2');
});

queueMicrotask(() => {
  console.log('Microtask 1');
});

console.log('End');

// Output:
// Start
// End
// Microtask 1        ← queueMicrotask = microtask
// Promise 1          ← Promise.then = microtask
// Promise 2          ← chained microtask
// setTimeout 1       ← macrotask
// setTimeout 2       ← macrotask

Code Explanation

Async/Await vs Promise Timing

async function example() {
  console.log('A: Inside async function');

  // Await pauses here; execution leaves the async function
  // But it goes to a microtask, not a macrotask
  await Promise.resolve();

  console.log('B: After await');
}

example();
console.log('C: After calling example');

// Output:
// A: Inside async function
// C: After calling example
// B: After await ← await = microtask

// Difference from setTimeout
async function example2() {
  console.log('1: Start');

  setTimeout(() => {
    console.log('2: setTimeout');
  }, 0);

  await Promise.resolve();

  console.log('3: After await');
}

example2();
console.log('4: Outside');

// Output:
// 1: Start
// 4: Outside
// 3: After await ← await = microtask, faster than setTimeout
// 2: setTimeout ← macrotask

requestAnimationFrame

// requestAnimationFrame runs BEFORE the browser renders
// and after microtasks
// Ideal for animations and UI updates

console.log('Start');

requestAnimationFrame(() => {
  console.log('requestAnimationFrame');
});

Promise.resolve().then(() => {
  console.log('Promise');
});

setTimeout(() => {
  console.log('setTimeout');
}, 0);

// Output:
// Start
// Promise          ← microtask (fastest)
// requestAnimationFrame ← before rendering
// setTimeout       ← macrotask (slowest)

Real-World Examples

Understanding Execution Order for UI Updates

// COMMON MISTAKE: updating the UI before the DOM is ready
function loadDashboard() {
  // This doesn't update the DOM immediately
  fetch('/api/data')
    .then(r => r.json())
    .then(data => {
      document.querySelector('#counter').textContent = data.count;
    });
}

// CORRECT: make sure the DOM is ready first
document.addEventListener('DOMContentLoaded', function () {
  loadDashboard();
});

Understanding When the UI Freezes

// FREEZES THE UI — all execution is synchronous
function processLargeData(data) {
  console.log('Start process...');
  // This loop BLOCKS the call stack until it's done
  for (let i = 0; i < data.length; i++) {
    // Heavy processing — the UI freezes during this
    calculateStatistics(data[i]);
  }
  console.log('Processing complete');
}

// SOLUTION: break it up with setTimeout or chunking
function processDataChunk(data, chunkSize = 100) {
  let index = 0;

  function processChunk() {
    const chunk = data.slice(index, index + chunkSize);

    chunk.forEach(item => calculateStatistics(item));
    index += chunkSize;

    // Give the UI a chance to update
    if (index < data.length) {
      setTimeout(processChunk, 0); // yield to the event loop
    } else {
      console.log('All done');
      updateUI();
    }
  }

  processChunk();
}

// SOLUTION: use a Web Worker for heavy computation
// (see the Web Workers tutorial)

Unbounded Microtasks

// CAREFUL: unbounded microtasks can block macrotasks
function endlessMicrotask() {
  Promise.resolve().then(() => {
    endlessMicrotask(); // endless recursion
  });
}

// The setTimeout will NEVER be called because the microtasks
// keep piling up
setTimeout(() => {
  console.log('This will never appear');
}, 0);

endlessMicrotask();

Click Event + Promise Order

// When the user clicks a button
document.querySelector('#btn').addEventListener('click', async function () {
  console.log('1: Button click');

  // Await inside the event handler
  const data = await fetch('/api/data').then(r => r.json());

  console.log('2: Data direceive');

  // Update the DOM
  document.querySelector('#output').textContent = data.value;

  console.log('3: DOM diupdate');
});

// Click the button → output:
// 1: Button click
// 2: Data direceive  ← after the fetch resolves
// 3: DOM diupdated

// What happens:
// 1. Click → the event handler enters the call stack
// 2. fetch() starts, the handler leaves the call stack (waiting)
// 3. The fetch resolves → Promise.then (microtask) → the handler continues
// 4. "Data direceive" is printed
// 5. The DOM is updated, "DOM diupdate" is printed

Execution Order Summary

// 1. Synchronous code (Call Stack) — runs first
// 2. Microtask Queue — processed to completion once the call stack is empty
//    - Promise.then/finally/catch
//    - queueMicrotask()
//    - MutationObserver
// 3. requestAnimationFrame — before rendering
// 4. Rendering — the browser repaints
// 5. Macrotask Queue — processed one at a time
//    - setTimeout/setInterval
//    - I/O callbacks
//    - UI event handlers

Conclusion

The Event Loop is the heart of JavaScript. The Call Stack executes synchronous code, Web APIs handle async operations, and the Microtask Queue (Promises) is processed before the Macrotask Queue (setTimeout). Use await to yield control to the event loop, setTimeout(fn, 0) to split up heavy work, and avoid unbounded recursive microtasks that can block the UI.