Node.js is a JavaScript runtime built on Chrome's V8 engine that executes JavaScript outside the browser. Its defining characteristic is an event-driven, non-blocking I/O model, which makes it lightweight and efficient for building scalable network applications. Despite JavaScript being single-threaded, Node.js achieves concurrency through the event loop, a mechanism that continuously checks the call stack and the callback queue. When the call stack is empty, the event loop pulls callbacks from the queue and executes them, allowing I/O operations to proceed without blocking the main thread.
The event loop progresses through a sequence of phases on each iteration: timers, pending callbacks, idle and prepare, poll, check, and close callbacks. The timers phase runs callbacks scheduled with setTimeout and setInterval. The poll phase retrieves new I/O events and may block briefly while waiting for them. The check phase then executes setImmediate callbacks, and the close callbacks phase handles things like socket close events. Understanding this ordering is essential for predicting when asynchronous code will actually run.
Two commonly confused scheduling primitives are setImmediate and setTimeout(fn, 0). setImmediate fires in the check phase of the current iteration, after the poll phase completes, while setTimeout schedules the callback for the timers phase on the next iteration. Inside an I/O callback, setImmediate always fires before setTimeout. A separate primitive, process.nextTick, fires even sooner: it runs immediately after the current operation on the microtask queue, before the event loop continues at all. Although useful, overusing process.nextTick can starve I/O by recursively deferring the loop from moving forward.