javascript Coursejavascriptevent-loopasyncasynchronous-programmingweb-developmenttutorial

Understanding the JavaScript Event Loop: From Confusion to Clarity

β€’20 min read

Understanding the JavaScript Event Loop: From Confusion to Clarity

I remember the first time I wrote code that made a fetch request to an API and then immediately tried to use the response. I was confused when it didn't workβ€”the response was undefined. I thought JavaScript was broken, or maybe I was using the wrong syntax.

// My naive attempt
const data = fetch('/api/users');
console.log(data); // undefined? What?

That's when I learned about asynchronous programming in JavaScript, and it completely changed how I understood the language. But here's the thingβ€”understanding async code isn't just about learning async/await syntax. To truly master JavaScript, you need to understand the event loop, the mechanism that makes asynchronous operations possible in a single-threaded environment.

In this post, we're going to explore the JavaScript event loop from the ground up. We'll start with why JavaScript is single-threaded, how the event loop works under the hood, and most importantly, what I learned the hard way about writing async code that doesn't block your application.

Intended audience: JavaScript developers who want to understand asynchronous programming deeplyβ€”from developers who've used async/await but don't know how it works, to intermediate developers who want to understand the "why" behind JavaScript's concurrency model and avoid common performance pitfalls.

Table of Contents


Why JavaScript is Single-Threaded

Before we dive into the event loop, let's understand a fundamental truth about JavaScript: it's single-threaded.

This means JavaScript can only execute one piece of code at a time. Unlike languages like Java or C++ that can spawn multiple threads to run code in parallel, JavaScript has one main thread that executes your code line by line.

But Wait, How Does It Handle Multiple Things?

Here's where it gets interesting. If JavaScript is single-threaded, how can it:

  • Make API calls without blocking?
  • Handle user clicks while processing data?
  • Animate elements while fetching data?

The answer is the event loop. But before we get there, let's understand why JavaScript was designed this way.

Why Would They Design It This Way?

JavaScript was created in 1995 by Brendan Eich for Netscape Navigator. At the time, web browsers needed a simple scripting language that could:

  1. Run in a browser environment
  2. Manipulate the DOM safely
  3. Handle user interactions

If JavaScript had been multithreaded, imagine the chaos:

  • Two threads trying to modify the same DOM element simultaneously
  • Race conditions when handling user events
  • Complex synchronization primitives (locks, mutexes) that most web developers wouldn't understand

By keeping JavaScript single-threaded, the designers made it:

  • Simpler: No need to worry about thread synchronization
  • Safer: DOM manipulation is always sequential
  • Predictable: Code executes in a deterministic order

But this created a problem: what if you need to wait for something slow?

The Blocking Problem

When I was first learning JavaScript, I wrote code like this:

// This blocks the entire browser
function slowOperation() {
  const start = Date.now();
  while (Date.now() - start < 5000) {
    // Wait 5 seconds
  }
  console.log('Done!');
}

slowOperation();
console.log('This waits 5 seconds before appearing');

If you run this in a browser, the entire page freezes for 5 seconds. The browser can't respond to clicks, can't render updates, nothing. This is because the single thread is busy executing the while loop.

This is the problem the event loop solves.


Understanding the Event Loop

The event loop is JavaScript's way of handling asynchronous operations without blocking the main thread. Think of it like a restaurant:

The Restaurant Analogy

Imagine a single-threaded restaurant with one waiter (the main thread). Here's how it works:

  1. The waiter takes orders (executes your code)
  2. The waiter gives orders to the kitchen (sends async operations to Web APIs)
  3. The waiter continues serving other tables (keeps executing code)
  4. When food is ready, the kitchen rings a bell (callback is added to the queue)
  5. The waiter picks up the food when they're free (callback is executed)

The key insight: the waiter doesn't wait at the kitchen. They keep working, and when the food is ready, they pick it up.

This is exactly how the event loop works. Let me show you:

console.log('1. Start');

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

console.log('3. End');

// Output:
// 1. Start
// 3. End
// 2. Timeout callback

Even though the timeout is set to 0 milliseconds, it executes after the synchronous code finishes. This is the event loop in action.


How the Event Loop Works Under the Hood

The event loop has several components working together:

  1. Call Stack: Where your code executes
  2. Web APIs: Browser-provided APIs (setTimeout, fetch, DOM events)
  3. Callback Queue: Where callbacks wait to be executed
  4. Microtask Queue: High-priority callbacks (Promises, queueMicrotask)

Here's how they work together:

console.log('1. Synchronous');

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

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

console.log('4. Synchronous');

// Output:
// 1. Synchronous
// 4. Synchronous
// 3. Promise
// 2. setTimeout

Wait, why does the Promise execute before setTimeout? This is where microtasks come in, but let's start with the basics.

πŸ“Š Step-by-Step Execution Flow

Let's trace through what happens when JavaScript executes this code:

console.log('Start');

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

console.log('End');

Visual Execution Timeline:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    EXECUTION TIMELINE                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

STEP 1: Initial Execution
───────────────────────────
Call Stack:          Web APIs:        Microtask Queue:  Callback Queue:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ console.log β”‚      β”‚          β”‚     β”‚             β”‚   β”‚             β”‚
β”‚ ('Start')   β”‚      β”‚          β”‚     β”‚             β”‚   β”‚             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό (executes, prints "Start", pops)

Call Stack:          Web APIs:        Microtask Queue:  Callback Queue:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ setTimeout  β”‚      β”‚          β”‚     β”‚             β”‚   β”‚             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚                    β”‚
       β–Ό                    β–Ό
   (registers)      (starts 1000ms timer)
       β”‚                    β”‚
       β–Ό                    β”‚
   (pops)                   β”‚
                            β”‚
                            β–Ό (after 1000ms)
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ Timer done!  β”‚
                    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
                    Callback Queue:
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ setTimeout   β”‚
                    β”‚  callback    β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Call Stack:          Web APIs:        Microtask Queue:  Callback Queue:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ console.log β”‚      β”‚          β”‚     β”‚             β”‚   β”‚ setTimeout  β”‚
β”‚ ('End')     β”‚      β”‚          β”‚     β”‚             β”‚   β”‚  callback   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό (executes, prints "End", pops)

STEP 2: Call Stack Empty - Event Loop Takes Over
─────────────────────────────────────────────────
Call Stack:          Event Loop:      Microtask Queue:  Callback Queue:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚             β”‚      β”‚ Checking β”‚     β”‚             β”‚   β”‚ setTimeout  β”‚
β”‚   EMPTY     │◄─────│ Queues   β”‚     β”‚   EMPTY     β”‚   β”‚  callback   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β–²                    β”‚
       β”‚                    β”‚
       β”‚                    β–Ό
       β”‚            Process Macrotasks
       β”‚                    β”‚
       β”‚                    β–Ό
       β”‚            Move to Call Stack
       β”‚                    β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Call Stack:          Event Loop:      Microtask Queue:  Callback Queue:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ setTimeout  β”‚      β”‚          β”‚     β”‚             β”‚   β”‚             β”‚
β”‚ callback    β”‚      β”‚          β”‚     β”‚   EMPTY     β”‚   β”‚   EMPTY     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β–Ό (executes, prints "Timeout", pops)

FINAL OUTPUT:
Start
End
Timeout  (appears after 1 second)

Step 1: Call Stack Execution

  • console.log('Start') β†’ executes β†’ prints "Start" β†’ pops
  • setTimeout(...) β†’ executes β†’ registers callback with Web API β†’ pops
  • console.log('End') β†’ executes β†’ prints "End" β†’ pops

Step 2: Web API Processing

  • setTimeout callback waits 1000ms
  • After 1000ms, callback is moved to Callback Queue

Step 3: Event Loop

  • Checks if Call Stack is empty
  • If empty, takes callback from Callback Queue
  • Pushes callback to Call Stack
  • Callback executes β†’ prints "Timeout"

This is the event loop's job: continuously check if the call stack is empty, and if so, move callbacks from the queue to the stack.


The Call Stack, Web APIs, and Callback Queue

Let's break down each component:

The Call Stack

The call stack is where JavaScript executes your code. It's a LIFO (Last In, First Out) data structureβ€”like a stack of plates.

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

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

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

first();

// Call Stack during execution:
// [third]
// [second, third]
// [first, second, third]
// [first, second]
// [first]
// []

When a function is called, it's pushed onto the stack. When it returns, it's popped off. The call stack is synchronousβ€”it executes one thing at a time.

Web APIs

Web APIs are provided by the browser (or Node.js runtime). They include:

  • setTimeout / setInterval
  • fetch / XMLHttpRequest
  • DOM events (addEventListener)
  • requestAnimationFrame

These APIs run outside the JavaScript engine. When you call setTimeout, you're telling the browser: "Hey, run this callback after 1000ms." The browser handles the timing, not JavaScript.

// This doesn't block JavaScript
setTimeout(() => {
  console.log('This runs later');
}, 1000);

// This code continues executing immediately
console.log('This runs now');

The Callback Queue

When a Web API finishes (like a timeout expiring or a fetch completing), it adds a callback to the Callback Queue (also called the Task Queue or Macrotask Queue).

The event loop continuously checks:

  1. Is the call stack empty?
  2. If yes, take the first callback from the queue
  3. Push it onto the call stack
  4. Execute it
console.log('1');

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

console.log('4');

// Output: 1, 4, 2, 3
// Both timeouts are queued, then executed in order

Synchronous vs Asynchronous Code

Understanding the difference between synchronous and asynchronous code is crucial.

Synchronous Code

Synchronous code executes immediately and blocks until it's done:

console.log('1');
console.log('2');
console.log('3');

// Output: 1, 2, 3 (in order, immediately)

Each line waits for the previous one to finish.

Asynchronous Code

Asynchronous code doesn't block. It schedules work to happen later:

console.log('1');

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

console.log('3');

// Output: 1, 3, 2

The setTimeout callback doesn't execute immediatelyβ€”it's scheduled to run after the current code finishes.

Real-World Example: Fetching Data

Here's a practical example I encountered when building a dashboard:

// ❌ This doesn't work as expected
let userData;
fetch('/api/user')
  .then((response) => response.json())
  .then((data) => {
    userData = data;
  });

console.log(userData); // undefined - fetch hasn't completed yet!

The fetch call is asynchronous. It doesn't block, so console.log executes immediately, before the data arrives.

// βœ… This works correctly
fetch('/api/user')
  .then((response) => response.json())
  .then((data) => {
    console.log(data); // Data is available here
  });

Common Async Patterns in JavaScript

JavaScript has evolved several ways to handle asynchronous code:

1. Callbacks (The Old Way)

Callbacks were the original way to handle async operations:

setTimeout(() => {
  console.log('Callback executed');
}, 1000);

Problem: Callback hell

// Callback hell - hard to read and maintain
getUser(userId, (user) => {
  getPosts(user.id, (posts) => {
    getComments(posts[0].id, (comments) => {
      getReplies(comments[0].id, (replies) => {
        console.log(replies); // Finally!
      });
    });
  });
});

2. Promises (Better)

Promises provide a cleaner way to handle async operations:

fetch('/api/user')
  .then((response) => response.json())
  .then((user) => {
    return fetch(`/api/posts/${user.id}`);
  })
  .then((response) => response.json())
  .then((posts) => {
    console.log(posts);
  })
  .catch((error) => {
    console.error('Error:', error);
  });

Better, but still can get verbose with multiple async operations.

3. Async/Await (Modern)

async/await makes async code look synchronous:

async function loadUserData() {
  try {
    const userResponse = await fetch('/api/user');
    const user = await userResponse.json();

    const postsResponse = await fetch(`/api/posts/${user.id}`);
    const posts = await postsResponse.json();

    console.log(posts);
  } catch (error) {
    console.error('Error:', error);
  }
}

Much cleaner! But remember: async/await is just syntactic sugar over Promises. Under the hood, it still uses the event loop.


Why You Should Be Careful: Blocking the Event Loop

Here's the most important thing I learned: you can still block the event loop with synchronous code.

The Problem

Even though JavaScript has async capabilities, synchronous code still blocks. If you write a slow synchronous operation, nothing else can execute until it finishes.

// This blocks the entire browser for 5 seconds
function blockEventLoop() {
  const start = Date.now();
  while (Date.now() - start < 5000) {
    // Busy waiting - blocks everything!
  }
}

blockEventLoop();
console.log('This won't appear for 5 seconds');

Common Blocking Operations

I've made these mistakes:

1. Heavy Loops

// ❌ Blocks the event loop
function processLargeArray(array) {
  for (let i = 0; i < array.length; i++) {
    // Heavy computation
    const result = complexCalculation(array[i]);
  }
}

// βœ… Break it up with setTimeout or use Web Workers
function processLargeArrayAsync(array) {
  let index = 0;

  function processChunk() {
    const chunkSize = 100;
    const end = Math.min(index + chunkSize, array.length);

    for (let i = index; i < end; i++) {
      complexCalculation(array[i]);
    }

    index = end;

    if (index < array.length) {
      setTimeout(processChunk, 0); // Yield to event loop
    }
  }

  processChunk();
}

πŸ“Š Visual: Blocking vs Non-Blocking Processing

BLOCKING APPROACH (❌):
────────────────────────
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Process ALL items at once              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ [item1][item2][item3]...[item10000]β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚  Browser: FROZEN ❌                      β”‚
β”‚  User clicks: IGNORED ❌                β”‚
β”‚  UI updates: BLOCKED ❌                  β”‚
β”‚  Duration: 5 seconds (feels like 50s)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

NON-BLOCKING APPROACH (βœ…):
──────────────────────────
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Process in chunks                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”  ...    β”‚
β”‚  β”‚chunk1β”‚β†’ β”‚chunk2β”‚β†’ β”‚chunk3β”‚β†’ ...    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”˜         β”‚
β”‚     ↓         ↓         ↓               β”‚
β”‚  Yield     Yield     Yield              β”‚
β”‚     ↓         ↓         ↓               β”‚
β”‚  Browser: RESPONSIVE βœ…                 β”‚
β”‚  User clicks: HANDLED βœ…                β”‚
β”‚  UI updates: RENDERED βœ…                 β”‚
β”‚  Duration: 5 seconds (feels instant)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Timeline Comparison:
────────────────────
Blocking:    [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ] 5s frozen
Non-blocking: [β–ˆ][β–ˆ][β–ˆ][β–ˆ][β–ˆ][β–ˆ][β–ˆ] 5s responsive
              ↑  ↑  ↑  ↑  ↑  ↑  ↑
              Each chunk yields to event loop

2. Synchronous File Operations (Node.js)

// ❌ Blocks in Node.js
const fs = require('fs');
const data = fs.readFileSync('large-file.txt'); // Blocks!

// βœ… Non-blocking
const fs = require('fs');
fs.readFile('large-file.txt', (err, data) => {
  // Callback executes when file is read
});

3. Heavy DOM Manipulation

// ❌ Can block rendering
function renderManyElements() {
  for (let i = 0; i < 10000; i++) {
    const div = document.createElement('div');
    document.body.appendChild(div); // Blocks!
  }
}

// βœ… Use requestAnimationFrame or break it up
function renderManyElementsAsync() {
  let index = 0;

  function renderChunk() {
    const chunkSize = 100;
    const end = Math.min(index + chunkSize, 10000);

    for (let i = index; i < end; i++) {
      const div = document.createElement('div');
      document.body.appendChild(div);
    }

    index = end;

    if (index < 10000) {
      requestAnimationFrame(renderChunk);
    }
  }

  renderChunk();
}

How to Identify Blocking Code

If your application:

  • Freezes during operations
  • Becomes unresponsive
  • Drops frames in animations
  • Can't handle user input

You likely have blocking code. Use browser DevTools Performance tab to identify long tasks.


Microtasks vs Macrotasks: Understanding Priority

Here's where it gets interesting. Not all callbacks are created equal.

Macrotasks (Callback Queue)

Macrotasks include:

  • setTimeout / setInterval
  • DOM events
  • setImmediate (Node.js)
  • I/O operations

Microtasks (Microtask Queue)

Microtasks include:

  • Promise.then() / Promise.catch() / Promise.finally()
  • queueMicrotask()
  • MutationObserver

The Key Difference: Execution Order

Microtasks have higher priority than macrotasks. The event loop processes all microtasks before moving to the next macrotask.

πŸ“Š Priority Visualization

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    EXECUTION PRIORITY                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Priority Level 1 (Highest): SYNCHRONOUS CODE
─────────────────────────────────────────────
Call Stack executes immediately, line by line

Priority Level 2: MICROTASKS
─────────────────────────────
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Microtask Queue                 β”‚
β”‚  (Processed BEFORE macrotasks)       β”‚
β”‚                                      β”‚
β”‚  β€’ Promise.then()                   β”‚
β”‚  β€’ Promise.catch()                  β”‚
β”‚  β€’ Promise.finally()                β”‚
β”‚  β€’ queueMicrotask()                 β”‚
β”‚  β€’ MutationObserver                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β”‚ ALL microtasks execute
         β”‚ before ANY macrotask
         β–Ό

Priority Level 3: MACROTASKS
─────────────────────────────
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      Callback Queue                  β”‚
β”‚  (Processed AFTER microtasks)       β”‚
β”‚                                      β”‚
β”‚  β€’ setTimeout()                     β”‚
β”‚  β€’ setInterval()                   β”‚
β”‚  β€’ DOM events                       β”‚
β”‚  β€’ I/O operations                  β”‚
β”‚  β€’ setImmediate() (Node.js)        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Visual Execution Pattern:

Code Execution Order:
─────────────────────

1. Synchronous code
   ↓
2. ALL Microtasks (one by one)
   ↓
3. ONE Macrotask
   ↓
4. ALL Microtasks again (if any new ones)
   ↓
5. NEXT Macrotask
   ↓
... and so on
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 (microtask runs first!)
// 2. setTimeout (macrotask runs after)

Why This Matters

This priority system ensures that Promise callbacks execute as soon as possible, which is important for maintaining predictable async behavior.

// This can be surprising
setTimeout(() => console.log('timeout'), 0);

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

// Output:
// promise 1
// promise 2
// timeout

Even though the timeout was queued first, all Promise microtasks execute before it.

Real-World Impact

I once encountered a bug where I expected a setTimeout to run before a Promise callback, but it didn't:

// My incorrect assumption
setTimeout(() => {
  console.log('This should run first');
}, 0);

someAsyncFunction().then(() => {
  console.log('This should run second');
});

// Actually: Promise runs first!

Understanding microtasks vs macrotasks helped me fix this.


Common Mistakes I Made

Here are mistakes I've made and what I learned:

Mistake 1: Assuming Async Code Executes Immediately

// ❌ Wrong assumption
let data;
fetch('/api/data')
  .then((response) => response.json())
  .then((result) => {
    data = result;
  });

console.log(data); // undefined - fetch is still in progress!

Lesson: Async code doesn't block, but it also doesn't execute immediately. Always handle the result inside the callback/Promise.

Mistake 2: Blocking with Synchronous Loops

// ❌ Blocks the event loop
function processItems(items) {
  items.forEach((item) => {
    heavyComputation(item); // Blocks!
  });
}

// βœ… Yield to event loop periodically
async function processItemsAsync(items) {
  for (const item of items) {
    await new Promise((resolve) => setTimeout(resolve, 0));
    heavyComputation(item);
  }
}

Lesson: Break up heavy synchronous operations to keep the UI responsive.

Mistake 3: Not Understanding Microtask Priority

// ❌ Unexpected behavior
setTimeout(() => console.log('timeout'), 0);

Promise.resolve().then(() => {
  console.log('promise');
  // This runs BEFORE timeout, even though timeout was queued first
});

Lesson: Microtasks (Promises) always execute before macrotasks (setTimeout).

Mistake 4: Creating Too Many Microtasks

// ❌ Can starve macrotasks
function createManyPromises() {
  for (let i = 0; i < 1000; i++) {
    Promise.resolve().then(() => {
      // If this creates more promises, macrotasks never run
    });
  }
}

Lesson: Be mindful of microtask creationβ€”they can delay macrotasks significantly.


Best Practices: How to Leverage Async Code Effectively

Here's what I've learned about writing effective async code:

1. Use Async/Await for Readability

// βœ… Clean and readable
async function fetchUserData(userId) {
  try {
    const user = await fetch(`/api/users/${userId}`).then((r) => r.json());
    const posts = await fetch(`/api/users/${userId}/posts`).then((r) =>
      r.json(),
    );
    return { user, posts };
  } catch (error) {
    console.error('Failed to fetch user data:', error);
    throw error;
  }
}

2. Handle Errors Properly

// βœ… Always handle errors
async function loadData() {
  try {
    const data = await fetch('/api/data').then((r) => r.json());
    return data;
  } catch (error) {
    // Log error, show user-friendly message, etc.
    console.error('Error loading data:', error);
    return null; // Or throw, depending on your needs
  }
}

3. Don't Block the Event Loop

// βœ… Break up heavy operations
async function processLargeDataset(data) {
  const chunkSize = 1000;

  for (let i = 0; i < data.length; i += chunkSize) {
    const chunk = data.slice(i, i + chunkSize);
    processChunk(chunk);

    // Yield to event loop every chunk
    await new Promise((resolve) => setTimeout(resolve, 0));
  }
}

4. Use Promise.all for Parallel Operations

// βœ… Fetch multiple things in parallel
async function loadDashboard() {
  const [user, posts, notifications] = await Promise.all([
    fetch('/api/user').then((r) => r.json()),
    fetch('/api/posts').then((r) => r.json()),
    fetch('/api/notifications').then((r) => r.json()),
  ]);

  return { user, posts, notifications };
}

5. Understand When to Use Microtasks vs Macrotasks

// Use microtasks for immediate, high-priority callbacks
Promise.resolve().then(() => {
  // Runs as soon as possible
});

// Use macrotasks for lower-priority, delayed callbacks
setTimeout(() => {
  // Runs after current microtasks
}, 0);

Key Takeaways

Here's what you should remember about the JavaScript event loop:

  1. JavaScript is single-threaded: Only one piece of code executes at a time.
  2. The event loop enables async behavior: It continuously checks if the call stack is empty and moves callbacks from queues to the stack.
  3. Web APIs run outside JavaScript: Operations like setTimeout and fetch are handled by the browser/runtime, not JavaScript itself.
  4. Microtasks have priority: Promise callbacks execute before setTimeout callbacks, even if the timeout was queued first.
  5. Synchronous code still blocks: Heavy loops or synchronous operations can freeze your application.
  6. Break up heavy operations: Use setTimeout, requestAnimationFrame, or Web Workers to keep the UI responsive.
  7. Async/await is syntactic sugar: It makes async code look synchronous, but it still uses Promises and the event loop under the hood.
  8. Understand execution order: Knowing when code executes helps you write predictable async code.

What's Next?

Now that you understand the event loop, here are some related topics to explore:

  • Web Workers: True parallelism in JavaScript (separate threads)
  • Async Generators: Combining generators with async/await
  • Streams API: Handling large data asynchronously
  • Service Workers: Background processing for web apps

The event loop is fundamental to JavaScript. Understanding it will help you:

  • Write more performant code
  • Debug async issues more effectively
  • Make better architectural decisions
  • Understand how frameworks like React handle updates

Remember: the event loop is JavaScript's way of handling concurrency in a single-threaded environment. Once you understand it, async JavaScript becomes much clearer.


πŸ“Š Complete Event Loop Summary

Here's a final visual summary of everything we've covered:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              THE COMPLETE EVENT LOOP PICTURE                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚  Your Code      β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚
                             β–Ό
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚  Call Stack     β”‚ ◄─── Executes one at a time
                    β”‚  (Synchronous)  β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β”‚                         β”‚
         Async? β”‚                         β”‚ Sync?
                β”‚                         β”‚
                β–Ό                         β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚   Web APIs         β”‚      β”‚  Continue        β”‚
    β”‚  (Browser handles) β”‚      β”‚  executing       β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
               β”‚ When ready
               β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚                     β”‚
    β–Ό                     β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Microtask   β”‚    β”‚ Callback    β”‚
β”‚ Queue       β”‚    β”‚ Queue       β”‚
β”‚ (Priority)  β”‚    β”‚ (Lower)     β”‚
β”‚             β”‚    β”‚             β”‚
β”‚ β€’ Promises  β”‚    β”‚ β€’ setTimeoutβ”‚
β”‚ β€’ queueMicroβ”‚    β”‚ β€’ DOM eventsβ”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚                  β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                β”‚
                β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚  Event Loop   β”‚ ◄─── Continuously checks
        β”‚               β”‚
        β”‚  1. Stack     β”‚
        β”‚     empty?    β”‚
        β”‚  2. Process   β”‚
        β”‚     ALL       β”‚
        β”‚     microtasksβ”‚
        β”‚  3. Process   β”‚
        β”‚     ONE       β”‚
        β”‚     macrotask β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                β”‚
                β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚  Call Stack   β”‚ ◄─── Back to execution
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

KEY PRINCIPLES:
───────────────
1. Single-threaded: One thing at a time
2. Non-blocking: Web APIs handle async
3. Priority: Microtasks > Macrotasks
4. Continuous: Event loop never stops
5. Yielding: setTimeout(0) gives control back

The event loop is the heart of JavaScript's async model. Understanding it helps you:

  • Write non-blocking code
  • Debug async issues
  • Optimize performance
  • Build responsive applications

Happy coding! πŸš€

Written by Sandeep Reddy Alalla

Share your thoughts and feedback!