6 Common Mistakes with Macrotasks in Node.js

When working with Node.js, understanding how macrotasks work...

6 Common Mistakes with Macrotasks in Node.js

When working with Node.js, understanding how macrotasks work is crucial for writing efficient and bug-free code. The event loop, which is at the core of Node.js’ asynchronous behavior, has two primary types of tasks: microtasks (like process.nextTick and Promise callbacks) and macrotasks (like setTimeout, setImmediate, and I/O operations).

Macrotasks run after the currently executing script and all pending microtasks have completed. However, many developers unknowingly misuse macrotasks, leading to performance bottlenecks, unexpected behavior, or race conditions.

1. Assuming setTimeout(fn, 0) Executes Immediately

Many developers assume that calling setTimeout(fn, 0) will execute fn immediately after the current script. In reality, it only schedules the function to run in the next macrotask queue cycle, meaning microtasks (like resolved Promises) will execute first.

Example of Unexpected Behavior

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

Promise.resolve().then(() => console.log("Microtask: Promise resolved"));

console.log("Main script execution");

Output

Main script execution
Microtask: Promise resolved
Macrotask: setTimeout

Even though setTimeout was given a 0 delay, the microtask from the Promise runs first because microtasks always execute before macrotasks in the event loop cycle.

Fix

If you need a task to run before setTimeout, use process.nextTick or queueMicrotask.

2. Misusing setImmediate in I/O-Intensive Code

setImmediate(fn) schedules a function to run after I/O callbacks but before setTimeout(fn, 0). Many assume it's the fastest way to schedule a macrotask, but it’s only beneficial when working with I/O-heavy operations.

Wrong Assumption

fs.readFile("test.txt", "utf8", () => {
  setTimeout(() => console.log("setTimeout"), 0);
  setImmediate(() => console.log("setImmediate"));
});

Output

setImmediate
setTimeout

This happens because setImmediate executes before setTimeout(fn, 0) when inside an I/O callback.

Fix

If you need a task to execute as soon as I/O is done, setImmediate is fine. But if precise timing matters, consider restructuring your code instead of relying on event loop mechanics.

3. Blocking the Event Loop with Synchronous Code

Node.js relies on the event loop to process macrotasks asynchronously. However, if you introduce heavy synchronous operations, it can block macrotasks from executing.

Problem

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

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

Here, the setTimeout callback won’t execute until the loop completes, delaying all macrotasks.

Fix

Offload intensive tasks to worker threads or use setImmediate strategically to break up execution chunks.

function heavyTask(count) {
  if (count === 0) return;
  setImmediate(() => heavyTask(count - 1)); // Avoid blocking
}

setTimeout(() => console.log("Macrotask executed"), 0);
heavyTask(5);

4. Forgetting That setTimeout Minimum Delay Isn’t Guaranteed

A setTimeout(fn, 10) does not mean fn will run exactly after 10 milliseconds. If the event loop is busy, the callback will be delayed.

Misconception

console.time("timeout");
setTimeout(() => console.timeEnd("timeout"), 10);

Actual Output (Not Always 10ms)

timeout: 14.387ms

The delay depends on system load and event loop state.

Fix

For precise scheduling, use setImmediate for immediate execution after I/O or process.hrtime() for more accurate timing.

5. Overusing process.nextTick, Causing Starvation

process.nextTick(fn) allows a callback to run before any macrotasks in the next tick of the event loop. However, excessive use can starve macrotasks, preventing setTimeout, setImmediate, or I/O from executing.

Bad Example

function repeat() {
  process.nextTick(repeat); // This will never allow I/O or macrotasks to run
}

repeat();

setTimeout(() => console.log("This will never run"), 0);

This creates an infinite loop of microtasks, preventing setTimeout from executing.

Fix

Instead of process.nextTick, use setImmediate to allow the event loop to process macrotasks:

function repeat() {
  setImmediate(repeat); // Allows event loop to process other tasks
}

repeat();

6. Not Understanding Event Loop Phases for Timers and I/O

Developers often assume all asynchronous operations behave the same, but Node.js processes them in different event loop phases.

  • Timers phase: Executes setTimeout and setInterval callbacks

  • I/O callbacks phase: Executes I/O callbacks

  • Check phase: Executes setImmediate

  • Close phase: Executes closed handle callbacks

Example

setImmediate(() => console.log("setImmediate"));
setTimeout(() => console.log("setTimeout"), 0);
fs.readFile("test.txt", "utf8", () => console.log("I/O operation"));

Possible Output

I/O operation
setImmediate
setTimeout

Since setImmediate executes in the check phase, it runs before setTimeout, which is in the timers phase.

Fix

Know when each function executes instead of assuming order.

Conclusion

Misunderstanding macrotasks in Node.js can lead to delayed execution, performance bottlenecks, or unintended behavior. The key takeaways are:

  • setTimeout(fn, 0) isn’t immediate; microtasks execute first.

  • setImmediate is useful for post-I/O execution, but not always faster.

  • Blocking the event loop delays all macrotasks.

  • setTimeout delays aren’t guaranteed to be exact.

  • Overusing process.nextTick can starve macrotasks.

  • Understanding event loop phases helps predict execution order.

You may also like:

1) 5 Common Mistakes in Backend Optimization

2) 7 Tips for Boosting Your API Performance

3) How to Identify Bottlenecks in Your Backend

4) 8 Tools for Developing Scalable Backend Solutions

5) 5 Key Components of a Scalable Backend System

6) 6 Common Mistakes in Backend Architecture Design

7) 7 Essential Tips for Scalable Backend Architecture

8) Token-Based Authentication: Choosing Between JWT and Paseto for Modern Applications

9) API Rate Limiting and Abuse Prevention Strategies in Node.js for High-Traffic APIs

10) Can You Answer This Senior-Level JavaScript Promise Interview Question?

11) 5 Reasons JWT May Not Be the Best Choice

12) 7 Productivity Hacks I Stole From a Principal Software Engineer

13) 7 Common Mistakes in package.json Configuration

Read more blogs from Here

Share your experiences in the comments, and let’s discuss how to tackle them!