Exploring Node.js: How the Event Loop Determines Execution Order


Introduction
As a Node.js developer, understanding the event loop and execution order is crucial for writing efficient and bug-free applications. The event loop is the core mechanism that allows Node.js to perform non-blocking I/O operations despite JavaScript being single-threaded. Let's explore the key timing functions and their execution order
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#34495e',
'primaryTextColor': '#ffffff',
'primaryBorderColor': '#2c3e50',
'lineColor': '#2980b9',
'secondaryColor': '#e74c3c',
'tertiaryColor': '#8e44ad'
}
}}%%
flowchart TD
classDef highest fill:#e74c3c,stroke:#000,stroke-width:2px,color:#ffffff
classDef high fill:#f39c12,stroke:#000,stroke-width:2px,color:#000000
classDef medium fill:#3498db,stroke:#000,stroke-width:2px,color:#ffffff
classDef low fill:#2ecc71,stroke:#000,stroke-width:2px,color:#000000
classDef module fill:#34495e,stroke:#000,stroke-width:2px,color:#ffffff
A["JavaScript Code Execution"] --> B["Call Stack"]
B --> C{"Event Loop"}
C --> D["Microtasks Queue"]
D --> E["process.nextTick()"]
D --> F["Promise callbacks"]
C --> G["Macrotasks Queue"]
G --> H["setImmediate"]
G --> I["setTimeout/setInterval"]
G --> J["I/O Events"]
K["Module Loading"] --> L["CommonJS (sync)"]
K --> M["ES Modules (async)"]
E --> |"1st Priority"| N["Execution"]
F --> |"2nd Priority"| N
H --> |"3rd Priority"| N
I --> |"4th Priority"| N
J --> |"5th Priority"| N
L --> |"Blocks"| B
M --> |"Non-blocking"| C
class E highest
class F high
class H medium
class I,J low
class L,M module
%% Animations
click E call callback() "Execute Microtask"
click H call callback() "Execute Macrotask"
Key Timing Functions
1. process.nextTick()
This is not technically part of the event loop but runs immediately after the current operation completes. It has the highest priority and runs before any other timing functions.
2.Promise callbacks
Promise callbacks are executed in the microtask queue, making them a high-priority part of the event loop execution order. They run after process.nextTick() but before any macrotasks like setImmediate or setTimeout.
3. setImmediate()
setImmediate() schedules a callback to execute in the next iteration of the event loop. It runs in the check phase, after I/O events but before timers. While similar to setTimeout(fn, 0), setImmediate() is more efficient for executing callbacks immediately after I/O events.
Key characteristics of setImmediate():
Executes callbacks in the check phase of the event loop
Ideal for running code after I/O operations complete
More predictable than setTimeout(0) when used within I/O callbacks
Useful for breaking up CPU-intensive tasks into manageable chunks
When used inside an I/O cycle, setImmediate() callbacks are always executed before setTimeout() and setInterval() callbacks, making it particularly useful for I/O-related operations.
4. setTimeout() and setInterval()
These run in the timers phase of the event loop. Even setTimeout(fn, 0) will have a minimum delay of 1ms.
5. I/O event
I/O events are operations that interact with external resources like files, network, or databases. These events are processed in the I/O phase of the event loop after pending timers but before setImmediate() callbacks.
Examples
console.log("Start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
setImmediate(() => {
console.log("setImmediate");
});
process.nextTick(() => {
console.log("nextTick 1");
process.nextTick(() => {
console.log("nextTick 2");
});
});
console.log("End");
// Output
Start
End
nextTick 1
nextTick 2
setImmediate
setTimeout
Module Systems and Timing
CommonJS (require)
In CommonJS, modules are loaded synchronously, and the code is executed immediately during the require() call. This means timing functions in the main module execute after all required modules are loaded.
graph TD
A["require() call"] --> B["Check cache"]
B -->|"Found"| C["Return cached"]
B -->|"Not found"| D["Load & execute"]
D --> E["Cache & return"]
style A fill:#95a5a6
style B fill:#f1c40f
style C fill:#2ecc71
style D fill:#e74c3c
style E fill:#3498db
This diagram illustrates the synchronous nature of CommonJS module loading:
When require() is called, Node.js first checks if the module is cached
If cached, it returns the cached exports immediately
If not cached, it loads and executes the module code synchronously
The module.exports object is created and cached
Finally, the exports are returned to the caller
This process blocks the event loop until the module is fully loaded and executed.
ES Modules (import)
ES Modules are loaded asynchronously and are always in strict mode. Top-level await is supported, which can affect the timing of execution.
graph TD
A["Your Code"] --> B["Loading Phase"]
B --> C["Building Phase"]
C --> D["Running Phase"]
B --> E["Get All Required Files"]
E --> C
C --> F["Set Up Everything"]
F --> G["Connect All Parts"]
D --> H["Run Your Code"]
style A fill:#95a5a6
style B fill:#4ecdc4
style C fill:#f1c40f
style D fill:#2ecc71
style E fill:#3498db
style F fill:#e74c3c
style G fill:#9b59b6
style H fill:#f39c12
The diagram above illustrates the ES Modules execution process:
Construction Phase: Modules are parsed and dependencies are identified
Module Graph: A complete graph of all dependencies is created
Instantiation Phase: Module environments are created and exports/imports are linked
Evaluation Phase: Module code is executed, handling any top-level await operations
This asynchronous process allows for better optimization and parallel loading compared to CommonJS.
Summary
The Node.js event loop processes tasks in a specific order: process.nextTick(), Promise callbacks (microtasks), setImmediate(), setTimeout/setInterval (macrotasks), and I/O events
Microtasks (process.nextTick and Promises) have the highest priority and execute before macrotasks
setImmediate() is optimized for executing callbacks after I/O events, making it more efficient than setTimeout(0)
CommonJS modules load synchronously and block execution, while ES Modules load asynchronously allowing for parallel processing
Understanding the event loop and execution order is essential for writing efficient Node.js applications and avoiding callback scheduling issues
Subscribe to my newsletter
Read articles from Nimatullah Razmjo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Nimatullah Razmjo
Nimatullah Razmjo
Software Engineer with 9+ years of experience in the back-end, front-end, and DevOps generally focused on back-end