Node.js Fundamentals

1. Introduction ๐Ÿš€

Node.js is a JavaScript runtime that lets you run JavaScript outside the browser, on servers, desktops, and even microcontrollers. It powers everything from REST APIs to real-time chat apps and command-line tools.

This tutorial walks through Node's internals โ€” from the V8 engine to the event loop โ€” building a solid mental model you can rely on when debugging performance issues or designing scalable systems.

Information

You should already be comfortable with core JavaScript (functions, closures, promises) before starting this tutorial.

2. What is the Node.js Runtime? ๐ŸŒฑ

A runtime is the environment in which a program executes. Browsers provide a JavaScript runtime that includes the DOM, window, and fetch. Node.js replaces those browser APIs with server-oriented ones โ€” file system access, networking, and process control โ€” while reusing the same V8 JavaScript engine that powers Google Chrome.

  • Built on V8, Google's open-source JavaScript engine.
  • Adds an event-driven, non-blocking I/O layer via libuv.
  • Ships with a standard library: fs, http, path, crypto, and more.
  • Originally created in 2009 by Ryan Dahl.

Tip

Think of Node as: "V8 + libuv + C++ bindings + a JS standard library."

3. Node.js Architecture ๐Ÿ—๏ธ

Node's architecture combines several layers that work together to execute JavaScript efficiently while handling I/O asynchronously.

Node.js Runtime
Your JavaScript Code
Node.js Bindings (C/C++)
V8 Engine
libuv
Other C++ Libraries (c-ares, OpenSSL, zlib)

3.1 JavaScript Runtime Environment

The runtime environment is everything available to your code beyond the JavaScript language itself: global objects, modules, timers, and I/O primitives. Node's runtime environment is intentionally different from a browser's โ€” there's no window, but there is a process object.

3.2 V8 Engine

V8 is Google's JavaScript and WebAssembly engine, written in C++. It compiles JavaScript directly to machine code using JIT compilation, and manages memory allocation and garbage collection for JS objects.

  • Ignition: V8's interpreter, produces bytecode quickly.
  • TurboFan: V8's optimizing compiler for hot code paths.
  • Handles the Heap (objects) and Call Stack (execution frames).

3.3 libuv

libuv is a C library that gives Node its asynchronous, event-driven superpowers. It abstracts platform-specific I/O (epoll on Linux, kqueue on macOS, IOCP on Windows) behind a single consistent API.

Important

libuv provides the event loop, the thread pool, and async networking โ€” V8 alone has none of these.

4. Event-Driven Programming ๐Ÿ””

Node.js is built around events. Instead of blocking while waiting for something (a file read, a network request), Node registers a callback and moves on, invoking that callback when the event fires.

event-emitter-example.js

const EventEmitter = require('events');
const emitter = new EventEmitter();

emitter.on('greet', (name) => {
  console.log(`Hello, ${name}!`);
});

emitter.emit('greet', 'World');

4.1 Non-Blocking I/O

In a blocking model, each I/O operation halts execution until it completes. Node instead uses non-blocking I/O: operations like reading a file are delegated to libuv, and execution continues immediately โ€” the result arrives later via a callback, promise, or async/await.

ModelBehaviorThroughput
Blocking I/OWaits for each operation to finishLow under concurrency
Non-blocking I/ODelegates and continues immediatelyHigh under concurrency

4.2 Single-Threaded Execution

JavaScript in Node runs on a single main thread. This simplifies programming (no shared-memory race conditions in your JS code) but means CPU-heavy synchronous work blocks everything else, including incoming requests.

Warning

A single while(true) loop or a huge synchronous JSON.parse can freeze an entire Node server.

5. The Event Loop ๐Ÿ”

The event loop is the mechanism that allows Node to perform non-blocking I/O despite JavaScript being single-threaded. It continuously checks the call stack, various queues, and pending I/O, deciding what runs next.

5.1 Call Stack

The call stack tracks function calls using a LIFO structure. When a function is invoked, a frame is pushed; when it returns, the frame is popped. The event loop only picks up new work when the call stack is empty.

5.2 Callback Queue

Also called the macrotask queue's I/O sibling, this queue holds callbacks from completed asynchronous operations (timers, I/O) waiting for the call stack to clear before they execute.

5.3 Microtask Queue

The microtask queue holds Promise callbacks (.then, .catch, .finally) and queueMicrotask() callbacks. Crucially, the entire microtask queue is drained after every single call stack operation โ€” before the event loop moves to the next phase.

microtask-vs-macrotask.js

console.log('1: sync');

setTimeout(() => console.log('4: macrotask'), 0);

Promise.resolve().then(() => console.log('3: microtask'));

console.log('2: sync');

// Output order: 1, 2, 3, 4

5.4 Macrotask Queue

Macrotasks include setTimeout, setInterval, setImmediate, and I/O callbacks. Only one macrotask is processed per event loop tick, and the microtask queue is fully drained before and after it.

5.5 Thread Pool

Some operations โ€” file system access, DNS lookups (dns.lookup), certain crypto functions โ€” aren't natively async at the OS level on all platforms. libuv handles these using a thread pool, defaulting to 4 threads, configurable via UV_THREADPOOL_SIZE.

Best Practice

If your app is heavy on fs or crypto operations, consider increasing UV_THREADPOOL_SIZE to reduce contention.

5.6 Worker Threads Overview

The worker_threads module lets you run actual JavaScript in parallel threads with independent V8 instances โ€” useful for CPU-intensive tasks (image processing, complex calculations) that would otherwise block the main thread.

worker-example.js

const { Worker, isMainThread, parentPort } = require('worker_threads');

if (isMainThread) {
  const worker = new Worker(__filename);
  worker.on('message', (msg) => console.log('From worker:', msg));
} else {
  parentPort.postMessage('Hello from worker thread!');
}

6. Global Objects & Variables ๐ŸŒ

6.1 Global Objects

Node exposes several objects globally without requiring an import or require:

  • global โ€” the top-level namespace object (analogous to window in browsers).
  • process โ€” information and control over the current Node process.
  • Buffer โ€” for handling raw binary data.
  • console โ€” logging utilities.

6.2 Global Variables

Some identifiers look global but are actually module-scoped โ€” injected per-file by Node's module wrapper:

module-wrapper.js

(function (exports, require, module, __filename, __dirname) {
  // Your module code lives here
});
  • __dirname โ€” absolute path of the current directory.
  • __filename โ€” absolute path of the current file.
  • require and module โ€” only in CJS files.

7. Module System ๐Ÿ“ฆ

7.1 Overview

Node supports two module systems: the original CommonJS (CJS) and standardized ES Modules (ESM). Understanding both โ€” and their differences โ€” is essential for modern Node development.

my-app
package.json
index.js

7.2 CommonJS

7.3 ES Modules

math.js (CJS)

function add(a, b) {
  return a + b;
}

module.exports = { add };

index.js (CJS)

const { add } = require('./lib/math');
console.log(add(2, 3));

math.mjs (ESM)

export function add(a, b) {
  return a + b;
}

index.mjs (ESM)

import { add } from './lib/math.mjs';
console.log(add(2, 3));
FeatureCommonJSES Modules
Syntaxrequire / module.exportsimport / export
LoadingSynchronousAsynchronous
File extension.js / .cjs.mjs or "type": "module"
__dirnameAvailableNot available (use import.meta.url)

8. Runtime & Process Lifecycle โš™๏ธ

8.1 Runtime Lifecycle

A Node program's lifecycle starts by executing the entry file top to bottom, registering callbacks, timers, and listeners. The process stays alive as long as there's something in the event loop to process; once the queues are empty and no handles remain, Node exits naturally.

8.2 Process Lifecycle

The global process object exposes lifecycle events and controls:

process-lifecycle.js

process.on('exit', (code) => {
  console.log(`About to exit with code: ${code}`);
});

process.on('SIGINT', () => {
  console.log('Received SIGINT (Ctrl+C)');
  process.exit(0);
});

Caution

Avoid calling process.exit() abruptly in application code โ€” it can cut off pending I/O, like unflushed logs.

9. Memory Management ๐Ÿง 

V8 divides memory into the stack (primitive values, function call frames) and the heap (objects, closures, arrays). Node processes have a default heap size limit, configurable via the --max-old-space-size flag.

9.1 Garbage Collection

V8's garbage collector reclaims memory occupied by objects no longer reachable from the root. It uses a generational strategy:

  1. Scavenge (Minor GC): fast, frequent collection of the young generation (short-lived objects).
  2. Mark-Sweep-Compact (Major GC): slower collection of the old generation (long-lived objects).

Example

Objects that survive several Scavenge cycles get promoted to the old generation, where they're collected less frequently.

9.2 Runtime Performance

Common levers for improving Node performance include reducing synchronous work on the main thread, streaming large payloads instead of buffering them fully, and profiling with tools like --prof or the clinic toolkit.

10. Best Practices โœ…

  • Prefer async/await over deeply nested callbacks for readability.
  • Never block the event loop with heavy synchronous computation โ€” offload to worker_threads or a queue.
  • Always handle Promise rejections; unhandled rejections can crash the process.
  • Use streams for large files instead of reading them entirely into memory.
  • Keep dependencies updated and audit them with npm audit.

Best Practice

Read the official Node.js documentation for API details and version-specific behavior.

11. Common Mistakes โš ๏ธ

  • Using synchronous fs methods (e.g. fs.readFileSync) in request handlers.
  • Assuming setTimeout(fn, 0) runs immediately โ€” it still waits for the current stack and microtasks.
  • Forgetting that require caches modules, leading to unexpected shared state.
  • Mixing CommonJS and ES Modules without understanding interop rules.
  • Not handling backpressure when working with streams.

12. Frequently Asked Questions โ“

Question

Is Node.js single-threaded?

Answer

The main JavaScript execution is single-threaded, but I/O is handled by libuv's thread pool and OS-level async mechanisms โ€” and worker_threads can run genuinely parallel JS.

Question

Can Node.js handle CPU-intensive tasks?

Answer

Not efficiently on the main thread. Use worker_threads, child processes, or offload to external services for heavy computation.

Question

Is CommonJS deprecated?

Answer

No โ€” CommonJS remains fully supported and widely used, though ES Modules are the modern standard going forward.

13. Summary ๐Ÿ“

Summary

Node.js pairs the V8 engine with libuv to run JavaScript efficiently outside the browser using an event-driven, non-blocking model. A single-threaded event loop coordinates the call stack, microtask queue, and macrotask queue, while a thread pool and worker_threads handle work that can't run inline. Understanding the module system, memory management, and process lifecycle equips you to build reliable, performant Node applications.