Call Stack vs. Task Queue: How JS Multitasks

Posted by

A deep dive into how JavaScript “fakes” multitasking with a single thread, and why understanding it saves you from async headaches.

A deep dive into how JavaScript “fakes” multitasking with a single thread, and why understanding it saves you from async headaches.

Introduction

If you’ve ever seen JavaScript run an API call, handle user input, and update the DOM all at once — you might have wondered:

👉 Wait, isn’t JavaScript single-threaded? How is it multitasking?

The secret lies in the call stack, the task queue, and the event loop. Together, they let JS juggle multiple tasks without actual threads.

Get them wrong, and you’ll write race conditions, frozen UIs, or promises that never behave as expected. Get them right, and you’ll master async code like a pro.

In this post, we’ll break down:

  • What the call stack does.
  • What the task queue (and microtask queue) does.
  • How the event loop orchestrates them.
  • Real-world examples (timers, promises, fetch, React state).
  • Common pitfalls — and how to avoid them.

1. The Call Stack

The call stack is where JS keeps track of what function is currently running. It’s a LIFO stack: last function called, first one to finish.

Example

function a() {
b();
}
function b() {
console.log("Hello");
}
a();

Stack trace:

[a] → [b] → console.log
  • Call a → pushed on stack.
  • Inside a, call b → pushed on stack.
  • Inside b, call console.log → pushed on stack.
  • Once console.log finishes, it pops, then b pops, then a pops.

👉 If the call stack never empties, your program freezes → “Maximum call stack size exceeded”.


2. The Task Queue (a.k.a. Callback Queue / Macrotask Queue)

Not all tasks can finish immediately. Some are asynchronous — like timers, events, and network requests.

When these are ready, their callbacks are placed in the task queue.

Example

console.log("Start");

setTimeout(() => console.log("Timeout"), 0);

console.log("End");

Output:

Start
End
Timeout

Why?

  • setTimeout schedules a callback → goes to task queue.
  • JS keeps running until stack is empty.
  • Event loop checks queue → pushes callback onto stack only after stack is clear.

👉 Timers, DOM events, setInterval, setImmediate → all go to the task queue.


3. The Microtask Queue (Priority Lane)

Promises, queueMicrotask, and MutationObserver use the microtask queue, which has higher priority than the task queue.

Example

console.log("A");

setTimeout(() => console.log("B"), 0);

Promise.resolve().then(() => console.log("C"));

console.log("D");

Output:

A
D
C
B
  • Promise.then callback is a microtask → runs before the next macrotask (setTimeout).
  • That’s why C comes before B.

👉 Rule: Microtasks run right after the current stack clears, before the task queue is touched.


4. The Event Loop

The event loop is the traffic cop:

  1. Look at call stack → if not empty, keep running.
  2. If stack is empty →
  • Run all microtasks.
  • If no more microtasks → take one callback from the task queue, put it on the stack.

3. Repeat forever.

👉 This is why JS feels “concurrent” despite being single-threaded.


5. Real-World Examples

a) Network Requests

console.log("Fetch start");

fetch("https://jsonplaceholder.typicode.com/posts/1")
.then(res => res.json())
.then(data => console.log("Data:", data));

console.log("Fetch scheduled");

Order:

  1. "Fetch start"
  2. "Fetch scheduled"
  3. Response returns async → .then callback → goes into microtask queue.

b) React State Updates

setCount(count + 1);
console.log(count); // stale value!

Why stale? Because React batches state updates into the microtask queue (or equivalent). By the time the log runs, the update isn’t committed yet.

👉 Solution: Use functional updates:

setCount(prev => prev + 1);

c) Blocking the Stack

console.log("Start");

setTimeout(() => console.log("Timeout"), 0);

for (let i = 0; i < 1e9; i++) {} // 🔥 heavy loop

console.log("End");

Output:

Start
End
Timeout

But notice the delay: the heavy loop blocked the stack. The timer couldn’t run until stack was clear.

👉 Lesson: JS is single-threaded. Long-running tasks block everything. Use Web Workers or chunk work.


6. Common Pitfalls

Pitfall 1: Expecting setTimeout(fn, 0) to run immediately

It waits until the stack clears → runs later.

Pitfall 2: Forgetting microtask priority

setTimeout(() => console.log("Task"), 0);
Promise.resolve().then(() => console.log("Microtask"));

Output:

Microtask
Task

Pitfall 3: Mixing async/await with loops

for (let i = 1; i <= 3; i++) {
await fetch(`/api/${i}`);
console.log(i);
}

Runs sequentially (await pauses loop). For parallelism, collect promises first and await Promise.all(promises).


7. Visualizing the Flow (ASCII)

+-----------------+
| Call Stack |
+-----------------+
|
v
+-----------------+
| Microtask Queue |
+-----------------+
|
v
+-----------------+
| Task Queue |
+-----------------+
|
v
+-----------------+
| Event Loop |
+-----------------+
  • Stack runs synchronously.
  • Microtasks clear before task queue.
  • Event loop coordinates.

8. Performance & Best Practices

  • ✅ Use promises / async–await instead of callback pyramids.
  • ✅ Break heavy loops into chunks with setTimeout or requestIdleCallback.
  • ✅ Use microtasks (Promise.resolve().then(...)) for immediate async jobs.
  • ✅ Never block the stack with sync-heavy work.

9. Quick Reference Table


Conclusion

JavaScript may be single-threaded, but thanks to the call stack, task queue, and microtask queue, it feels multitasked. The event loop makes sure async tasks happen at the right time, in the right order.

Key takeaways:

  • The call stack runs sync code immediately.
  • Microtasks (promises) run before tasks (timers, events).
  • Long-running sync code blocks everything.
  • Mastering these rules = no more async surprises.

👉 Pro tip: When debugging async bugs, ask: Where is my code queued? Stack, microtask queue, or task queue? That’s usually the answer.


Call to Action

What’s the trickiest async bug you’ve faced — stale state in React, or setTimeout(fn, 0) not firing when expected?

💬 Share it in the comments.
🔖 Bookmark this for future debugging sessions.
👩‍💻 Share with your teammate who still thinks JS is truly multithreaded.

Leave a Reply

Your email address will not be published. Required fields are marked *