Difference between process.nextTick, setImmediate and setTimeout in Node.js

In here we explore differences between process.nextTick, setImmediate and setTimeout in Node.js by example.

12 min read

November 17th, 2024

Nick Mousavi

Event loop Visualization image source: Lydia hallie

Execution order in Event Loop

Event loop: is endless loop that JavaScript engine uses to execute tasks.

  1. Synchronous code
  2. Micro Tasks
  3. Macro Tasks

Synchronous code

Synchronous code or blocking code runs in the first order and blocks further execution untill it's done.

codes like console.log(), variable and function declarations are synchronous code.

code.js
console.log("Synchronous code");
const iAmAsynchronous = 10;
function justAFunction() {}

Micro Tasks

Tasks that are executed after the Synchronous code and before the next iteration of the event loop.

Common example is process.nextTick() in Node.js that runs after the synchronous code and before the next iteration in event loop.

other examples are Promise callbacks (.then, .catch, .finally)

process.nextTick always runs before Promise callbacks(.then, .catch, .finally).

console.log("1. Synchronous code"); // first
Promise.resolve().then(() => console.log("3. Promise then")); // third
process.nextTick(() => console.log("2. nextTick code")); // second

Note: Immediately after every macrotask, the engine executes all tasks from microtask queue, before running any other macrotasks.

that means, all the tasks in Microtask Queue until empty.

console.log("sync");

process.nextTick(() => console.log("nextTick"));

setTimeout(() => console.log("setTimeout")); // 👈 after all

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


// 1. sync
// 2. nextTick (microtask)
// 3. Promise then (microtask)
// 4. setTimeout (mAcrotask)

queueMicrotask

Also we have a special function called queueMicrotask(func) that queues func for execution in the microtask queue.

queueMicrotask(() => console.log("queue"));
  • use it to execute a function asynchronously, but within the environment state.

Macro Tasks

Microtask Queue image source: Lydia hallie

A macrotask is like putting a task on a "to-do later" list in JavaScript. When you create a macrotask, you're essentially saying "I'll deal with this later, after I'm done with what I'm doing right now (microTask)."

These are the examples of Macrotask:
  • setTimeout
  • setInterval
  • I/O operations
  • setImmediate
  • UI rendering
  • requestAnimationFrame

In each iteration or Tick of event loop, one macrotask will be processed.

So what's the difference between process.nextTick, setImmediate and setTimeout in Node.js

let's check it by another example:

// sync
console.log("Start");

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

// microtask 👈 after current event-loop, especially after I/O, before timers
setImmediate(() => console.log("setImmediate"));

// Executes before next tick of event loop
process.nextTick(() => console.log("nextTick"));

console.log("End");

// Start
// End
// nextTick
// setImmediate
// setTimeout

When to use?

1. process.nextTick

  • For immediate execution after current operation
  • higher priority in current execution compare to two others
  • Use when you need to handle errors, cleanup, or retry requests

2. setImmediate

  • Executes immediately after the current event loop iteration
  • It has higher priority than other I/O events and timers
  • when you want to execute something after I/O events but before setTimeout

3. setTimeout

  • Executes after a specified delay (in milliseconds)
  • It has a lower priority compared to setImmediate()
  • use it to break up CPU-intensive tasks
  • if you have a CPU-hungry task, you can split that into parts using (setTimeout), so you are not blocking the interface or other tasks.
let i = 0;
function count() {
  do {
    i++;
    progress.innerHTML = i;
    // do a piece of the heavy job (*)
  } while (i % 1e3 != 0);

  // so we don't block the progress bar
  if (i < 1e7) setTimeout(count);
}

Wrap-up

The execution order is:

1. All synchronous code runs to completion

2. Micro tasks queue is processed until empty

  • process.nextTick()
  • Promise callbacks (.then, .catch, .finally)

3. One macro task is processed

  • setimmediate
  • timers (setTimeout, setInterval)

4. Back to step 2 (micro tasks)

9 Asynchronous JavaScript and Event Loop Questions

In JavaScript, asynchronous operations refer to functions or tasks that execute in parallel with other functions or operations. These operations do not block the main thread of execution, allowing the program to continue running while waiting for their completion.

Callback functions are crucial in asynchronous JavaScript because they provide a mechanism to execute code after the asynchronous operation has finished. Since asynchronous operations might take time to complete, callback functions ensure that specific actions are taken only when the operation is done, preventing race conditions and ensuring proper program flow.

The Web API extends the capabilities of the JavaScript engine by providing functionalities that are not inherently part of the language. These functionalities, including the DOM API, setTimeout, and HTTP requests, enable asynchronous, non-blocking behavior.

For instance, when using setTimeout, the JavaScript engine relies on the browser's Web API to handle the timer. The Web API keeps track of the specified delay, and when the time is due, it adds the callback function to the queue for execution without blocking the main JavaScript thread.

The asynchronous execution flow in JavaScript involves a coordinated interaction between these components:

  • Call Stack: This is where function calls are placed and executed sequentially. When a function is invoked, it's pushed onto the stack, and when it returns, it's popped off.
  • Web API: This provides functionalities like setTimeout that enable asynchronous behavior. When an asynchronous operation is initiated, the Web API manages it outside the call stack.
  • Queue: When an asynchronous operation completes in the Web API, its corresponding callback function is placed in the queue, waiting to be executed.
  • Event Loop: The event loop continuously monitors the call stack and the queue. If the call stack is empty, indicating that the main thread is free, the event loop takes the first item from the queue and pushes it onto the call stack for execution.

This interplay ensures that asynchronous operations don't block the main thread, and their callbacks are executed when the call stack is free, maintaining a responsive and efficient program flow.

In Node.js, Microtask Queues are used to manage the execution of small, high-priority tasks that need immediate attention after the current iteration of the Event Loop completes. Unlike tasks in the Callback Queue, microtasks do not need to wait for other tasks to finish before executing. This immediate execution ensures microtasks don’t block the Event Loop and run efficiently.

The key difference lies in their priority:

  • Microtasks have higher priority than tasks in the Callback Queue. They are executed before the next macrotask is processed, making them suitable for handling time-sensitive operations.
  • The Callback Queue handles tasks that come from functions like setTimeout after their timers expire.

Microtask Queues are divided into two categories:

  • process.nextTick()
  • Promises

process.nextTick() is a method in Node.js used to schedule a callback function to be executed in the next iteration of the Event Loop. This means the callback function is added to the microtask queue and is executed before any other I/O events or timers. Although it schedules for the next iteration, it effectively runs before the next "tick" of the Event Loop, giving it higher priority than setImmediate or setTimeout.

process.nextTick() is often used when you need to perform an action immediately after the current operation completes, but before the Event Loop continues to other tasks. Common use cases include:

  • Handling errors: process.nextTick() can be used to handle errors that occur during asynchronous operations. By deferring the error handling to process.nextTick(), you can ensure that the current operation completes before the error is thrown.
  • Cleanup: Similar to error handling, process.nextTick() can be used to perform cleanup tasks after an asynchronous operation completes.
  • Retry requests: In situations where you need to retry a failed request, process.nextTick() can be used to schedule the retry attempt immediately after the previous attempt fails.

Both setImmediate() and setTimeout() are functions in Node.js that allow you to execute code asynchronously, but they differ in their execution timing and priority within the Event Loop:

  • setImmediate(): This function schedules a callback function to be executed immediately after the current poll phase of the Event Loop completes. It has a higher priority than other I/O events and timers, ensuring its execution before them.
  • setTimeout(): This function schedules a callback function to be executed after a specified delay (in milliseconds). It has a lower priority compared to setImmediate(), and other I/O events or microtasks might be executed before the setTimeout callback, even if the delay is set to 0.

Use Cases:

  • setImmediate(): When you want to execute something after I/O events but before the next round of timers (setTimeout).
  • setTimeout(): Useful for breaking up CPU-intensive tasks to prevent them from blocking the Event Loop for extended periods.

CPU-intensive tasks can be divided into smaller chunks to avoid blocking the user interface. This is achieved by scheduling each chunk to run using setTimeout with a delay of 0 milliseconds.

While a 0-millisecond delay might suggest immediate execution, it actually gets deferred due to the minimum 4ms delay inherent in browsers for nested setTimeout calls. This delay creates space for the Event Loop to process other tasks, maintaining UI responsiveness.

Code Example:

let i = 0;
let start = Date.now();

function count() {
  // Do a piece of the heavy job
  do {
    i++;
  } while (i % 1e6 != 0);

  if (i == 1e9) {
    alert("Done in " + (Date.now() - start) + 'ms');
  } else {
    setTimeout(count); // Schedule the new call
  }
}

count();

In this example, the count function processes a portion of the task and schedules its next execution using setTimeout. This breaks down the intensive task, allowing other events and UI updates to occur between chunks, ensuring a smooth user experience.

Macrotasks and microtasks are both asynchronous tasks in JavaScript, but they differ in their priority and execution order within the Event Loop:

  • Macrotasks: Examples include setTimeout, setInterval, I/O operations, setImmediate, and UI rendering. In each iteration of the Event Loop, one macrotask is dequeued and processed. After processing a macrotask, the Event Loop checks the microtask queue before proceeding to the next macrotask.
  • Microtasks: Microtasks are usually created by promises and are executed immediately after the current macrotask completes, but before the next macrotask is processed. Examples include process.nextTick(), Promise callbacks (.then, .catch, .finally), and the queueMicrotask(func) function.

Execution Order:

  1. All synchronous code runs to completion.
  2. The microtask queue is processed until empty.
  3. One macrotask is processed.
  4. The process repeats from step 2 (processing microtasks).

This order ensures that microtasks, often associated with promise resolutions and immediate actions, are handled promptly, while macrotasks, representing tasks scheduled for later execution, are processed one at a time, preventing any single task from monopolizing the Event Loop.

queueMicrotask(func) and setTimeout both offer ways to execute code asynchronously in JavaScript, but they differ in their scheduling and timing within the Event Loop. Choosing between them depends on the specific needs of your application:

  • queueMicrotask(func): This function schedules a microtask to be executed immediately after the current macrotask is completed, but before any rendering or other macrotasks take place. Microtasks are executed in the same context as the current macrotask.
  • setTimeout: This schedules a macrotask to be executed after a specified delay (which could be 0 milliseconds). However, due to the minimum delay inherent in browsers, even a 0-millisecond delay results in a deferred execution. Macrotasks are executed in a separate context.

Choose queueMicrotask(func) when:

  • You need a function to run asynchronously after the current code, but before any rendering updates or new events are handled.
  • You require the function to execute in the same environment state as the current code.

Choose setTimeout when:

  • You want to introduce a deliberate delay before executing the function.
  • You are splitting a large task into smaller chunks to avoid blocking the UI thread.
  • You need to schedule an action after an event has been fully handled (bubbling complete).
Resources and References

biomousavi gist

https://javascript.info/

Lydia hallie

GeekForGeeks

Tags:NodeJS
Nick Mousavi
A small step forward is still progress.

To get in touch with Nick Mousavi,
please enter your email address below.

© 2020-present Nick Mousavi. All Rights Reserved.

Privacy Policy