Getting Started
REPL & Running Scripts
The REPL (Read-Eval-Print Loop) is an interactive shell for experimenting with Node.js code. Use node --watch for development auto-restart. ES modules require type:module in package.json or .mjs extension.
# start the REPL (interactive shell)
node
# run a script
node app.js
# run with ES modules (type: module in package.json)
node --input-type=module app.js
# execute inline code
node -e "console.log(process.version)"
# watch mode (auto-restart on change)
node --watch app.jsVersion & Runtime Flags
node --version prints the runtime version. --trace-warnings shows stack traces for runtime warnings. NODE_OPTIONS is a single env var holding CLI flags, handy inside containers or npm scripts. --inspect opens the Chrome DevTools debugger.
# check version
node --version # v20.11.0
node -p "process.versions.v8"
# enable warning stack traces
node --trace-warnings app.js
# pass NODE_OPTIONS (max ~128KB string)
NODE_OPTIONS="--max-old-space-size=4096" node app.js
# inspect memory usage at runtime
node --inspect app.jspackage.json & Project Init
npm init creates package.json. The "type" field decides the default module system: "module" enables ES module syntax in .js files, "commonjs" (default) keeps require/module.exports. "main" is the entry point when the package is required.
# scaffold a project (answer prompts)
npm init
# non-interactive defaults
npm init -y
# "type" controls module system
# "commonjs" (default) -> require/module.exports
# "module" -> import/export
{
"name": "my-app",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"scripts": { "start": "node index.js" }
}CommonJS vs ES Modules
Node supports two module systems. CommonJS (require/module.exports) is synchronous and the historical default. ES Modules (import/export) are the modern standard. The file extension (.mjs/.cjs) or package.json "type" field decides which parser is used for .js files.
# CommonJS (.cjs or type:commonjs) ----
const fs = require('fs'); // import
module.exports = { greet: () => 'hi' }; // export
# ES Modules (.mjs or type:module) -------
import fs from 'fs'; // import
export function greet() { return 'hi'; } // named export
export default { greet }; // default export
# .mjs is always ESM, .cjs is always CJS
node file.mjs # forced ESM
node file.cjs # forced CJSGlobal Objects
Node provides globals like process, console, Buffer, and timers without imports. __dirname and __filename are CommonJS-only — in ES Modules use import.meta.url instead. global is the root scope object (like window in browsers).
# available everywhere, no import needed
global; // global namespace object
process; // env, argv, stdin/stdout
console; // log, error, warn, table
setTimeout; // schedule a callback
setInterval; // schedule repeating callback
Buffer; // binary data (Uint8Array)
queueMicrotask;// schedule microtask
structuredClone; // deep clone
# CommonJS-only globals (not in ESM)
__dirname; // current directory
__filename; // current file pathExecutable Scripts (Shebang)
A shebang line (#!/usr/bin/env node) lets a file run as a standalone command on Unix. After chmod +x, run ./cli.js directly. The package.json "bin" field maps a command name to a script, used by CLI tools published to npm so global install exposes the command.
#!/usr/bin/env node
// cli.js - make a file directly runnable
const name = process.argv[2] || 'world';
console.log(`Hello, ${name}!`);
# make it executable (Unix) and run directly
chmod +x cli.js
./cli.js Alice # Hello, Alice!
# or wire it up in package.json
{
"bin": { "mycli": "./cli.js" }
}Modules System
CommonJS require & exports
require() synchronously loads a CommonJS module and returns its module.exports. require resolves relative paths (./, ../), built-ins (fs), and node_modules. Always assign to module.exports (not exports) when replacing the whole export, because exports is just an alias for module.exports initially.
// math.js
function add(a, b) { return a + b; }
const PI = 3.14;
// export via module.exports
module.exports = { add, PI };
// or: exports.add = add; exports.PI = PI;
// app.js
const math = require('./math'); // .js optional
console.log(math.add(1, 2)); // 3
console.log(math.PI); // 3.14
// destructuring
const { add } = require('./math');
console.log(add(5, 7)); // 12ES Modules import/export
ES Modules use static import/export — bindings are live and hoisted. The default export is imported with any name (no braces); named exports must use braces with matching names. import * as creates a namespace object. ESM is async by design and the modern standard.
// math.mjs (or type:module)
export function add(a, b) { return a + b; }
export const PI = 3.14;
export default function square(x) { return x * x; }
// app.mjs
import square, { add, PI } from './math.mjs';
console.log(add(1, 2)); // 3
console.log(square(5)); // 25
// import all as namespace
import * as math from './math.mjs';
console.log(math.PI); // 3.14Dynamic import()
import() is a dynamic, async import usable in both CJS and ESM. It returns a Promise resolving to the module namespace, enabling lazy-loading and conditional imports — great for reducing startup time or loading optional features. Use it for code-splitting in servers too.
// dynamic import returns a Promise
async function loadPlugin(name) {
const mod = await import(`./plugins/${name}.mjs`);
return mod.default;
}
// lazy-load heavy modules on demand
if (needPdf) {
const { exportPdf } = await import('./pdf.mjs');
exportPdf(data);
}
// works in both CommonJS and ESM
import('./optional.mjs')
.then(mod => mod.run())
.catch(err => console.error('load failed', err));Built-in Modules
Node ships dozens of built-in modules — no npm install needed. Most expose callback APIs; many also offer promise variants under the 'module/promises' path (fs/promises, dns/promises, stream/promises). require('module') (no ./) resolves to built-ins first, then node_modules.
// core modules - no install, just require/import
const fs = require('fs'); // file system
const path = require('path'); // path handling
const http = require('http'); // HTTP server/client
const os = require('os'); // OS info
const crypto = require('crypto'); // hashing/encryption
const { EventEmitter } = require('events');
const { Buffer } = require('buffer');
const stream = require('stream'); // streams
const net = require('net'); // TCP
const child_process = require('child_process');
// some are promise-based (Node 10+)
const fsp = require('fs/promises'); // async/await fsModule Caching & Circular Deps
require() caches modules by resolved path, so the same module code runs once and exports are reused. Circular dependencies work but the first-loaded module sees an incomplete (partial) exports object of the second — design around them. delete require.cache to hot-reload during development.
// modules are cached after first load
// a.js
require('./b'); // b loads, sets module.exports
const b = require('./b'); // returns cached b (same object)
console.log(b === require('./b')); // true
// circular dependency: a -> b -> a
// a.js: const b = require('./b'); b.f();
// b.js: const a = require('./a'); // a is PARTIAL here!
// module.exports = { f: () => a.x };
// inspect the cache
console.log(require.cache); // map of loaded modules
delete require.cache[require.resolve('./b')]; // force reload__dirname & import.meta.url
__dirname and __filename are CommonJS-only globals giving the current folder/file path. ES Modules have no such globals — derive them from import.meta.url via fileURLToPath. import.meta.url is the file:// URL of the current module and the ESM replacement for __filename.
// CommonJS - synchronous path globals
console.log(__dirname); // /app/src
console.log(__filename); // /app/src/index.js
// ES Modules - no __dirname, use import.meta
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const dataPath = join(__dirname, 'data.json');
// import.meta also carries the module URL
console.log(import.meta.url); // file:///app/src/index.mjsnpm & pnpm
npm init & package.json
npm init scaffolds a package.json. -y skips prompts with defaults. npm set stores config in ~/.npmrc for future inits. Scoped packages (@org/name) are useful for organizations and avoiding name collisions on the registry.
# create package.json interactively
npm init
# accept all defaults
npm init -y
# pnpm equivalent
pnpm init
# set author/license globally
npm set init-author-name "Alice"
npm set init-license MIT
# scope packages with @username
npm init --scope=@myorgInstalling Dependencies
npm install (no args) installs all listed deps. With a name it adds to package.json. --save-dev puts it in devDependencies (test/build tools, not shipped). ^ allows minor+patch updates, ~ allows patch only. -g installs CLI tools globally.
# install everything in package.json
npm install # alias: npm i
pnpm install # alias: pnpm i
# add a runtime dependency -> dependencies
npm install express
pnpm add express
# add a dev-only dependency -> devDependencies
npm install --save-dev jest # npm i -D jest
pnpm add -D jest
# install a specific version
npm install [email protected]
npm install lodash@^4.17.0 # caret: 4.x
npm install lodash@~4.17.0 # tilde: 4.17.x
# global install (CLI tools)
npm install -g pm2
pnpm add -g pm2npm scripts
Scripts in package.json run via npm run <name>. start and test are special (npm start / npm test). pre/post hooks (pretest, postbuild) run automatically. Use -- to forward args to the underlying command. pnpm lets you skip the word "run" for custom scripts.
// package.json
{
"scripts": {
"start": "node index.js",
"dev": "node --watch index.js",
"test": "jest",
"build": "tsc && vite build"
}
}
# run a script
npm run dev # npm run-script dev
pnpm dev # pnpm has no "run" requirement
# "start" and "test" have shortcuts
npm start
npm test
# pre/post hooks run automatically
// "pretest": "npm run lint",
// "postbuild": "npm run deploy"
# pass extra args with --
npm run build -- --mode productionnpx & pnpm dlx
npx runs a package binary without a global install — it downloads, executes, and discards. Perfect for scaffolders (create-vite) and running local node_modules/.bin tools. --yes skips the install confirmation. pnpm dlx is the pnpm counterpart.
# run a one-off command without installing globally
npx create-vite my-app
npx cowsay "hello"
# pin a version
npx prettier@3 --write .
# pnpm equivalent
pnpm dlx create-vite my-app
# execute a local bin from node_modules
npx jest # runs ./node_modules/.bin/jest
# never prompt, always download latest
npx --yes create-react-app my-appVersioning & lockfiles
SemVer: MAJOR for breaking changes, MINOR for features, PATCH for fixes. ^ (caret) is npm's default and allows minor+patch updates; ~ (tilde) only patch. Lockfiles (package-lock.json) freeze the full dependency tree for reproducible installs — use npm ci in CI for speed and safety.
# semantic versioning: MAJOR.MINOR.PATCH
# 1.4.2 -> 1:breaking 4:feature 2:fix
"^1.4.2" # >=1.4.2 <2.0.0 (caret, default)
"~1.4.2" # >=1.4.2 <1.5.0 (tilde)
"1.4.2" # exact 1.4.2
">=1.4.0 <2.0.0" # range
# lockfiles pin the installed tree
npm install # writes/updates package-lock.json
pnpm install # writes pnpm-lock.yaml
yarn install # writes yarn.lock
# install exactly as locked (CI)
npm ci # clean install from lockfile
# find outdated deps
npm outdated
npm update # update within ^ rangesPublishing & Lifecycle
npm publish uploads your package to the registry. npm version bumps the version, creates a git tag, and commits. Lifecycle hooks (prepublishOnly, postpublish) automate build/test on release. Unpublish is only allowed within 72 hours — prefer npm deprecate for old versions.
# login once
npm login
# publish to the registry
npm publish
npm publish --access public # scoped default restricted
# bump version (updates package.json + tag + commit)
npm version patch # 1.0.0 -> 1.0.1
npm version minor # 1.0.0 -> 1.1.0
npm version major # 1.0.0 -> 2.0.0
# lifecycle scripts (in package.json)
// "prepublishOnly": "npm test && npm run build",
// "prepublish": "npm run build",
// "postpublish": "git push --tags"
# unpublish within 72h
npm unpublish [email protected]File System (fs)
Reading Files
fs offers sync (readFileSync), callback (readFile), and promise (fs/promises readFile) APIs. Sync calls block the event loop — avoid in servers. fs/promises is the modern choice for async/await. Without an encoding, readFile returns a Buffer.
const fs = require('fs');
// synchronous (blocks the event loop)
const text = fs.readFileSync('data.txt', 'utf8');
const buf = fs.readFileSync('image.png'); // Buffer
// asynchronous with callback (error-first)
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) return console.error(err);
console.log(data);
});
// async with fs/promises (preferred)
const fsp = require('fs/promises');
async function read() {
const text = await fsp.readFile('data.txt', 'utf8');
return text;
}Writing Files
writeFile replaces the whole file by default. The flag option changes behavior: 'a' appends, 'wx' fails (EEXIST) if the file already exists — useful for atomic file creation. JSON should be stringified before writing. Sync writes block; prefer fs/promises.
const fs = require('fs');
const fsp = require('fs/promises');
// sync
fs.writeFileSync('out.txt', 'hello');
// async callback
fs.writeFile('log.txt', 'line1\n', err => {
if (err) throw err;
});
// promise
await fsp.writeFile('config.json', JSON.stringify(cfg, null, 2));
// flags: 'w' write (default), 'a' append, 'wx' fail if exists
await fsp.writeFile('new.txt', 'x', { flag: 'wx' }); // throws if existsDirectories
mkdir with { recursive: true } mimics mkdir -p. readdir lists entries; { withFileTypes: true } returns Dirent objects whose isDirectory()/isFile() avoid extra stat calls. fsp.rm with recursive+force replaces rmdir for non-empty dirs (Node 14+).
const fsp = require('fs/promises');
// create (recursive like mkdir -p)
await fsp.mkdir('a/b/c', { recursive: true });
// list entries
const entries = await fsp.readdir('.');
// ['app.js', 'data', 'package.json']
// list with type info
const detailed = await fsp.readdir('.', { withFileTypes: true });
detailed.forEach(e => console.log(e.name, e.isDirectory()));
// remove
await fsp.rmdir('empty'); // empty dir only
await fsp.rm('folder', { recursive: true, force: true }); // rm -rf
// sync versions exist too: mkdirSync, readdirSync, rmSyncFile Stats & Metadata
stat() returns a Stats object with size, mtime, and type helpers. Don't use fs.exists (deprecated); use access() in a try/catch for race-free existence checks — though opening the file directly is safer. fsWatch emits change events but can be inconsistent across platforms.
const fsp = require('fs/promises');
const stats = await fsp.stat('data.txt');
stats.isFile(); // true
stats.isDirectory(); // false
stats.size; // bytes
stats.mtime; // Date modified
stats.birthtime; // Date created
stats.mode; // permission bits
// check existence (avoid existsSync for races)
try {
await fsp.access('config.json');
console.log('exists');
} catch {
console.log('missing');
}
// watch for changes
const watcher = fs.watch('.');
watcher.on('change', (eventType, filename) => {});fs.promises API
fs/promises (Node 10+) provides promise-based versions of all fs methods, ideal for async/await. opendir() returns an async iterator for memory-efficient directory traversal. Combine with path.join for portable paths. All methods throw on error — wrap in try/catch.
const fsp = require('fs/promises');
const path = require('path');
// full async/await file operations
async function loadConfig() {
const file = path.join(__dirname, 'config.json');
const raw = await fsp.readFile(file, 'utf8');
return JSON.parse(raw);
}
async function saveConfig(cfg) {
const file = path.join(__dirname, 'config.json');
await fsp.writeFile(file, JSON.stringify(cfg, null, 2));
}
// iterate directory tree
for await (const entry of await fsp.opendir('.')) {
console.log(entry.name);
}Append, Rename, Delete
appendFile adds content to the end (creates the file if missing). rename is atomic on the same filesystem and used for moving files. unlink deletes a file. copyFile copies content; fsp.cp (Node 16+) handles directories recursively. truncate resizes a file to the given length.
const fsp = require('fs/promises');
// append to a file (creates if missing)
await fsp.appendFile('log.txt', new Date() + ' started\n');
// atomic rename (also for moving)
await fsp.rename('old.txt', 'new.txt');
// move across dirs
await fsp.rename('a/x.txt', 'b/x.txt');
// delete a file
await fsp.unlink('temp.txt');
// copy
await fsp.copyFile('src.txt', 'dest.txt');
// or fsp.cp('src', 'dest', { recursive: true })
// truncate to a length
await fsp.truncate('data.bin', 1024);Path Handling
path.join & path.resolve
path.join concatenates segments using the platform separator and normalizes the result. path.resolve produces an absolute path starting from process.cwd(); an absolute segment discards previous segments. Always use path.join instead of string concatenation so paths work on Windows and POSIX.
const path = require('path');
// join segments with the OS separator
path.join('src', 'utils', 'file.js');
// 'src/utils/file.js' (Linux) or 'src\utils\file.js' (Windows)
// resolve to an ABSOLUTE path (from cwd)
path.resolve('src', 'file.js');
// '/current/dir/src/file.js'
// resolve with absolute segment resets
path.resolve('/a', 'b', '/c', 'd'); // '/c/d'
// normalize '..' and '.' segments
path.normalize('a/b/../c/./d'); // 'a/c/d'
// never hardcode '/' — use path.join for portabilitydirname, basename, extname
dirname returns the directory, basename the file name (optionally stripping an extension), and extname the last extension (including the dot). For multi-part extensions like .tar.gz only the final .gz is returned. These are pure string operations — no filesystem access.
const path = require('path');
const file = '/app/src/utils/helper.js';
path.dirname(file); // '/app/src/utils'
path.basename(file); // 'helper.js'
path.basename(file, '.js'); // 'helper' (strip ext)
path.extname(file); // '.js'
path.extname('archive.tar.gz'); // '.gz' (last ext)
// for the current file (CommonJS)
path.dirname(__filename); // current dir
path.basename(__filename); // current file namepath.parse & format
path.parse splits a path into root, dir, base, name, and ext. path.format reassembles them. path.relative computes the relative path needed to go from one directory to another. Together they make path manipulation declarative rather than fiddly string slicing.
const path = require('path');
// decompose a path into parts
const p = path.parse('/app/src/helper.test.js');
// { root: '/', dir: '/app/src',
// base: 'helper.test.js', name: 'helper.test', ext: '.js' }
// rebuild from a parsed object
const rebuilt = path.format({
dir: '/app/dist',
name: 'bundle',
ext: '.min.js'
});
// '/app/dist/bundle.min.js'
// get the relative path from A to B
path.relative('/a/b/c', '/a/x'); // '../../x'Cross-platform Paths
path.sep and path.delimiter differ per OS. path.win32 and path.posix force a specific parser regardless of the host OS — handy when processing paths from another platform. Use path.delimiter to split PATH-style env vars portably instead of hardcoding ':' or ';'.
const path = require('path');
// platform-specific values
path.sep; // '/' (POSIX) or '\\' (Windows)
path.delimiter; // ':' (POSIX) or ';' (Windows)
// force a specific platform's behavior
path.win32.join('a', 'b'); // 'a\\b'
path.posix.join('a', 'b'); // 'a/b'
// parse PATH env var portably
process.env.PATH.split(path.delimiter).forEach(dir => {
console.log(dir);
});
// check if absolute
path.isAbsolute('/etc'); // true
path.isAbsolute('a/b'); // false
path.win32.isAbsolute('C:\\'); // trueFile URLs & pathToFileURL
ES Modules are identified by file:// URLs. Use url.pathToFileURL to turn a normal path into a URL for dynamic import(), and url.fileURLToPath to go the other way. In ESM, import.meta.url is already a file URL — convert it to a path with fileURLToPath to replicate __filename.
const path = require('path');
const url = require('url');
// convert a path to a file:// URL (ESM import)
const fileUrl = url.pathToFileURL('/app/src/mod.mjs');
// URL { href: 'file:///app/src/mod.mjs' }
// and back
const p = url.fileURLToPath('file:///app/src/mod.mjs');
// '/app/src/mod.mjs'
// in ESM, import.meta.url is already a file URL
// convert to a normal path:
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);HTTP Server
Basic HTTP Server
http.createServer returns a server; the callback handles every request. res.writeHead sets the status code and headers, res.end sends the body and finishes the response. listen() starts accepting connections on the given port. One Node process handles many concurrent requests via the event loop.
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello, World!\n');
});
// listen on a port
server.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
// or with a host and backlog
server.listen(3000, '127.0.0.1', 511, () => {});Request & Response
req exposes method, url, and headers; res lets you set statusCode/headers then write+end. res.end MUST be called to finish the response. For large payloads, stream data via res.write or pipe a Readable into res to avoid buffering everything in memory.
const http = require('http');
http.createServer((req, res) => {
// request info
console.log(req.method); // 'GET'
console.log(req.url); // '/users?id=5'
console.log(req.headers); // { host, 'user-agent', ... }
// response helpers
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.write('partial chunk\n');
res.end(JSON.stringify({ ok: true }));
}).listen(3000);
// streaming a large response
res.writeHead(200);
readStream.pipe(res); // pipe file -> responseRouting & URL Parsing
Node has no built-in router — implement one with if/else on req.method and req.url, or use the URL constructor to parse pathname and searchParams. For real apps, reach for Express or the router in a framework. The URL constructor needs a base when parsing a relative req.url.
const http = require('http');
const { URL } = require('url');
http.createServer((req, res) => {
const url = new URL(req.url, 'http://localhost');
const path = url.pathname;
const q = url.searchParams.get('id');
if (req.method === 'GET' && path === '/') {
res.end('home');
} else if (path === '/users') {
res.end('users');
} else {
res.writeHead(404);
res.end('not found');
}
}).listen(3000);Handling JSON & POST Body
Request bodies arrive as a stream — you must collect chunks before parsing. The async-iterator pattern (for await of req) is the modern approach. Always set Content-Type: application/json when responding with JSON. For production, limit body size to prevent memory exhaustion.
const http = require('http');
http.createServer(async (req, res) => {
if (req.method === 'POST') {
// collect the streamed body
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const body = JSON.parse(Buffer.concat(chunks).toString());
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ received: body }));
}
}).listen(3000);
// test: curl -X POST localhost:3000 -d '{"a":1}'Server Events & Errors
The server is an EventEmitter. 'error' must be handled — EADDRINUSE means the port is taken. 'connection' fires per client socket. For graceful shutdown, listen for SIGTERM/SIGINT and call server.close() so in-flight requests finish before the process exits.
const server = http.createServer(handler);
server.on('listening', () => console.log('ready'));
server.on('connection', socket => console.log('client connected'));
server.on('error', err => {
if (err.code === 'EADDRINUSE') {
console.error('port 3000 already in use');
}
});
server.on('close', () => console.log('server closed'));
// graceful shutdown
process.on('SIGTERM', () => {
server.close(() => process.exit(0));
});
server.listen(3000);HTTPS & HTTP Client
https.createServer requires a private key and certificate (e.g. from Let's Encrypt). For outgoing requests, Node 18+ ships a global fetch() — prefer it over the raw http.get callback API. fetch is promise-based and supports async/await, headers, and streams.
// HTTPS server (needs TLS certs)
const https = require('https');
const fs = require('fs');
https.createServer({
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
}, handler).listen(443);
// HTTP client: simple GET
http.get('http://example.com', res => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => console.log(data));
});
// modern fetch (Node 18+, global, promise-based)
const res = await fetch('https://api.example.com');
const json = await res.json();Express Basics
Express Setup & Hello World
Express is the most popular Node web framework. app.get registers a route handler; res.send sends a response (auto-detecting Content-Type). app.listen starts the server. Express wraps Node's http module with routing, middleware, and convenience helpers.
// install: npm install express
import express from 'express';
const app = express();
const PORT = 3000;
app.get('/', (req, res) => {
res.send('Hello, World!');
});
app.listen(PORT, () => {
console.log(`Server on http://localhost:${PORT}`);
});Routing & Route Params
Express matches HTTP methods (get/post/put/delete) to paths. :id captures URL segments into req.params; req.query holds query-string values. app.route lets you chain multiple methods for the same path. Routes match in registration order, so define specific routes before generic ones.
app.get('/users', (req, res) => res.send('list users'));
app.post('/users', (req, res) => res.send('create user'));
// URL parameters
app.get('/users/:id', (req, res) => {
res.send(`user ${req.params.id}`);
});
// query strings: /search?q=node
app.get('/search', (req, res) => {
res.send(`searching: ${req.query.q}`);
});
// chain handlers for one path
app.route('/book')
.get((req, res) => res.send('get book'))
.post((req, res) => res.send('add book'));Middleware
Middleware are functions that run during the request cycle, receiving req, res, and next. Call next() to continue to the next handler. app.use mounts middleware globally or on a path prefix. Error-handling middleware has 4 args (err first) and should be registered last.
// middleware: (req, res, next) => {}
app.use(express.json()); // parse JSON bodies
app.use(express.static('public'));// serve static files
app.use((req, res, next) => { // custom logger
console.log(req.method, req.url);
next(); // pass control forward
});
// mounted on a path
app.use('/api', (req, res, next) => {
req.apiCall = true;
next();
});
// error-handling middleware (4 args)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Server Error');
});Request Data
Express parses bodies only if you mount the right middleware: express.json() for JSON, express.urlencoded() for forms, multer for file uploads. req.params (URL segments), req.query (query string), and req.body (parsed payload) are the three main request-data sources.
// JSON body (needs express.json() middleware)
app.post('/api', (req, res) => {
console.log(req.body); // parsed JSON object
});
// URL params and query
app.get('/u/:id', (req, res) => {
const { id } = req.params;
const { tab } = req.query;
});
// form data (urlencoded)
app.use(express.urlencoded({ extended: true }));
app.post('/form', (req, res) => res.json(req.body));
// file uploads: use multer middleware
// const upload = multer({ dest: 'uploads/' });
// app.post('/upload', upload.single('file'), handler);
// headers & cookies
req.headers['user-agent'];
req.cookies; // needs cookie-parserServing Static Files
express.static serves files from a directory. Mount multiple times for fallback behavior, or prefix with a path. Options like maxAge set caching headers. It auto-detects MIME types and supports range requests for video/audio. Always use absolute paths to avoid cwd-dependent bugs.
const path = require('path');
// serve files from /public at /
app.use(express.static('public'));
// GET /style.css -> public/style.css
// mount at a prefix
app.use('/static', express.static('public'));
// GET /static/style.css -> public/style.css
// multiple directories (fallthrough)
app.use(express.static('public'));
app.use(express.static('uploads'));
// absolute path + options
app.use(express.static(path.join(__dirname, 'public'), {
maxAge: '1d',
setHeaders: (res, filePath) => {
if (filePath.endsWith('.html')) res.setHeader('Cache-Control', 'no-cache');
}
}));Error Handling & Router
Pass an error to next(err) or throw inside an async handler to trigger error-handling middleware. Register one central 4-arg handler last. express.Router() modularizes routes into separate files — mount them with app.use('/prefix', router) to compose a large API cleanly.
// throw to jump to error middleware
app.get('/broken', (req, res, next) => {
const err = new Error('boom');
err.status = 400;
next(err); // or throw err (Express 4+)
});
// centralized error handler (last middleware)
app.use((err, req, res, next) => {
const status = err.status || 500;
res.status(status).json({ error: err.message });
});
// modular routing with express.Router()
const router = express.Router();
router.get('/', (req, res) => res.send('api root'));
router.get('/users', (req, res) => res.send('users'));
app.use('/api', router); // mount at /apiStreams
Readable Streams
A Readable stream produces data. In flowing mode, 'data' events push chunks automatically; in paused mode, you call read() explicitly. Always handle 'error' — unhandled error events crash the process. highWaterMark controls the internal buffer size.
const fs = require('fs');
// create a readable stream from a file
const rs = fs.createReadStream('big.log', { highWaterMark: 64 * 1024 });
// 'data' mode (flowing)
rs.on('data', chunk => {
console.log('got', chunk.length, 'bytes');
});
rs.on('end', () => console.log('done'));
rs.on('error', err => console.error(err));
// paused mode: pull on demand
rs.on('readable', () => {
let chunk;
while ((chunk = rs.read()) !== null) {
process(chunk);
}
});Writable Streams
A Writable stream consumes data. write() returns false when the internal buffer is full — wait for the 'drain' event before writing more to respect backpressure. end() signals no more data; 'finish' fires after everything is flushed. process.stdout is itself a writable stream.
const fs = require('fs');
const ws = fs.createWriteStream('out.log');
// write returns false when the buffer is full (backpressure)
const ok = ws.write('line\n');
if (!ok) ws.once('drain', () => writeMore());
// signals completion
ws.end('final line\n');
ws.on('finish', () => console.log('all data flushed'));
ws.on('error', err => console.error(err));
// process.stdout is a writable stream
process.stdout.write('hello');pipe & pipeline
pipe() forwards a Readable to a Writable with automatic backpressure. stream.pipeline (prefer the promises version) chains multiple streams AND properly propagates errors and cleans up — .pipe() alone leaks resources on mid-stream errors. Always prefer pipeline over manual .pipe() chains.
const fs = require('fs');
const { pipeline } = require('stream/promises');
// pipe: readable -> writable (auto backpressure)
fs.createReadStream('in.txt')
.pipe(fs.createWriteStream('out.txt'));
// pipeline: chain transforms, with error handling
const { Transform } = require('stream');
const upper = new Transform({
transform(chunk, enc, cb) { cb(null, chunk.toString().toUpperCase()); }
});
await pipeline(
fs.createReadStream('in.txt'),
upper,
fs.createWriteStream('out.txt')
);
// pipeline rejects on error, unlike .pipe()Transform Streams
A Transform stream reads input, processes it, and emits transformed output — perfect for parsing, compressing, or encrypting in a pipeline. Implement transform(chunk, enc, cb) and optionally flush(cb). objectMode lets you push objects instead of Buffers. zlib and crypto provide ready-made transforms.
const { Transform } = require('stream');
// a Transform is both Readable and Writable
const csvToJson = new Transform({
objectMode: false,
transform(chunk, encoding, callback) {
// convert each chunk
const lines = chunk.toString().trim().split('\n');
const json = lines.map(l => JSON.stringify(l.split(','))).join('\n');
callback(null, json + '\n');
},
flush(callback) {
callback(null, ']\n'); // finalize
}
});
process.stdin.pipe(csvToJson).pipe(process.stdout);
// built-in transforms: zlib, crypto streams
const { createGzip } = require('zlib');
fs.createReadStream('log').pipe(createGzip()).pipe(fs.createWriteStream('log.gz'));Stream Modes & Backpressure
Streams alternate between flowing and paused modes via pause()/resume(). Backpressure means a slow consumer signals the producer to slow down — async iteration (for await) handles this automatically by awaiting each chunk, the cleanest modern pattern. Always set an encoding to get strings instead of Buffers.
const rs = fs.createReadStream('huge.bin');
// switch between modes
rs.pause();
rs.resume();
rs.setEncoding('utf8');
// respect backpressure manually
function writeChunk(ws, data) {
if (!ws.write(data)) {
rs.pause();
ws.once('drain', () => rs.resume());
}
}
// async iteration (Node 10+) — cleanest pattern
async function consume() {
for await (const chunk of rs) {
await handle(chunk); // naturally respects backpressure
}
}
consume();Custom Streams
Subclass Readable/Writable/Duplex and implement _read/_write, or use the simpler Readable.from() for data you already have. push(null) signals end-of-stream. Duplex streams are readable AND writable independently (like TCP sockets); Transform is a Duplex where output derives from input.
const { Readable, Writable, Duplex } = require('stream');
// custom Readable: generate a counter
const counter = new Readable({
read() {
if (++this._n > 10) this.push(null); // end
else this.push(String(this._n));
}
});
counter._n = 0;
counter.pipe(process.stdout);
// from an array / async iterable
Readable.from(['a', 'b', 'c']).pipe(process.stdout);
// a Duplex: both readable and writable (e.g. a socket)
class Echo extends Duplex {
_read() {}
_write(chunk, enc, cb) { this.push(chunk); cb(); }
}Buffer
Creating Buffers
Buffer.alloc creates a zero-filled buffer (safe). Buffer.allocUnsafe skips zeroing for speed but may leak old memory — only use it when you'll overwrite every byte. Buffer.from converts strings/arrays/buffers. The legacy 'new Buffer()' constructor is deprecated due to security concerns.
// allocate zeroed bytes (SAFE)
const b1 = Buffer.alloc(8); // <Buffer 00 00 00 00 00 00 00 00>
// allocate WITHOUT zeroing (faster, may contain old memory)
const b2 = Buffer.allocUnsafe(8); // random old data!
// from a string with encoding
const b3 = Buffer.from('hello', 'utf8');
// <Buffer 68 65 6c 6c 6f>
// from an array of bytes
const b4 = Buffer.from([0x48, 0x49]); // 'HI'
// from another buffer (copy)
const b5 = Buffer.from(b3);
// NEVER use new Buffer() — deprecated and unsafeReading & Writing
Buffers hold raw bytes. Use read/write methods with explicit sizes (UInt8/16/32) and endianness (BE/LE). Big-endian is network byte order; little-endian is common on x86. toString(encoding, start, end) decodes bytes to a string. Always pass an offset to avoid overwriting earlier bytes.
const buf = Buffer.alloc(8);
// write numeric values at an offset
buf.writeUInt8(255, 0); // 1 byte
buf.writeUInt16BE(0x1234, 1); // 2 bytes, big-endian
buf.writeInt32LE(1000, 3); // 4 bytes, little-endian
// read them back
buf.readUInt8(0); // 255
buf.readUInt16BE(1); // 0x1234
buf.readInt32LE(3); // 1000
// string read/write
buf.write('Hi', 0, 'utf8');
buf.toString('utf8', 0, 2); // 'Hi'Concat & Compare
Buffer.concat joins an array of buffers (an optional total-length hint improves performance). equals/compare let you compare byte sequences; indexOf/includes find sub-buffers — useful in binary protocols. copy(target, targetStart, sourceStart, sourceEnd) copies a slice into another buffer.
const a = Buffer.from('foo');
const b = Buffer.from('bar');
// concatenate buffers
const c = Buffer.concat([a, b]); // 'foobar'
Buffer.concat([a, b], 6); // total length hint
// compare
Buffer.compare(a, b); // -1 (a < b), 0, or 1
a.equals(b); // false
// find a sub-buffer
const big = Buffer.from('hello world');
big.indexOf(Buffer.from('world')); // 6
big.includes(Buffer.from('hello')); // true
// copy bytes
const target = Buffer.alloc(5);
big.copy(target, 0, 0, 5); // copy first 5 bytesEncodings
Encodings map between strings and bytes. utf8 is the default for text. base64/hex are common for binary-in-text (data URLs, hashes, transport). Buffer.byteLength gives the byte size (multi-byte chars count more than one byte in utf8) — use it when sizing buffers, not string.length.
const text = 'Hello, 世界';
// string -> buffer (encode)
const utf8 = Buffer.from(text, 'utf8');
const base64 = Buffer.from(text).toString('base64');
const hex = Buffer.from(text).toString('hex');
const latin1 = Buffer.from(text, 'latin1');
// buffer -> string (decode)
utf8.toString('utf8'); // 'Hello, 世界'
Buffer.from(base64, 'base64').toString('utf8');
Buffer.from(hex, 'hex').toString('utf8');
// common encodings: utf8, utf16le, latin1, ascii,
// base64, base64url, hex
Buffer.byteLength('世界', 'utf8'); // 6 bytes (not 2)Buffer & TypedArrays
Buffer extends Uint8Array, so it works wherever TypedArrays do. Buffer.from(arrayBuffer) shares memory (no copy) — but a Buffer may slice a larger pooled ArrayBuffer, so always honor byteOffset/byteLength when creating views. This lets you mix Buffer APIs with DataView for fine-grained numeric access.
// Buffer is a subclass of Uint8Array
const buf = Buffer.from([1, 2, 3]);
buf instanceof Uint8Array; // true
// share memory with TypedArrays without copying
const u8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
view.getInt32(0);
// Buffer from an ArrayBuffer
const ab = new ArrayBuffer(8);
const buf2 = Buffer.from(ab);
// note: a Buffer's underlying buffer may be larger
// (allocUnsafe pools) -> always use byteOffsetSlice, Fill, Swap
subarray (or slice) returns a view sharing the original memory — mutations affect both. fill writes a byte or repeating string across the buffer. swap16/swap32 reverse byte order in place for endianness conversion. Buffers are iterable, yielding each byte value 0–255.
const buf = Buffer.from('abcdef');
// slice shares memory (no copy) — like subarray
const s = buf.subarray(1, 4); // 'bcd'
s[0] = 88; // mutates buf too -> 'aXcdef'
// fill with a value or pattern
Buffer.alloc(4).fill(0xff); // <Buffer ff ff ff ff>
Buffer.alloc(6).fill('ab'); // 'ababab'
// reverse byte order in place (for endianness swaps)
const n = Buffer.from([0x01, 0x02, 0x03, 0x04]);
n.swap16(); // swaps pairs: <Buffer 02 01 04 03>
// iterate with for...of
for (const byte of buf) console.log(byte); // 97, 98, ...EventEmitter
Creating & Emitting Events
EventEmitter is Node's pub/sub core. emit(eventName, ...args) fires an event; on() subscribes. Many built-in classes (streams, http server) extend it. Event names are arbitrary strings, though 'error' is special. Listeners are called synchronously in registration order.
const { EventEmitter } = require('events');
class Clock extends EventEmitter {
start() {
this.emit('start', Date.now()); // emit with args
setInterval(() => this.emit('tick', new Date()), 1000);
}
}
const clock = new Clock();
clock.on('tick', date => console.log('tick', date));
clock.on('start', ts => console.log('started at', ts));
clock.start();
// on() == addListener; once() fires only onceonce & removeListener
once() registers a listener that auto-removes after firing — great for one-time setup. To remove a listener you must keep a reference to the exact function (arrow functions create new refs each time). removeAllListeners clears listeners for one or all events. off is an alias for removeListener.
const ee = new EventEmitter();
// once: auto-removed after first fire
ee.once('init', () => console.log('init fired'));
// remove a specific listener (need the same function ref)
const handler = () => console.log('hi');
ee.on('msg', handler);
ee.off('msg', handler); // off() == removeListener
// remove all listeners for an event (or all events)
ee.removeAllListeners('msg');
ee.removeAllListeners(); // everything
// inspect
ee.listenerCount('msg');
ee.eventNames(); // ['msg', 'init']Error Events
If an 'error' event is emitted and NO listener is registered, Node throws and crashes the process. Always attach an error handler to EventEmitters — especially streams and HTTP servers. captureRejections (Node 12+) routes rejected promises from async listeners to the 'error' event.
const ee = new EventEmitter();
// 'error' is special: if unhandled, it CRASHES the process
ee.on('error', err => {
console.error('caught:', err.message);
});
// later, emit an error safely
ee.emit('error', new Error('something broke'));
// guard against crashes from unhandled errors
// (monitor with the static capture)
EventEmitter.captureRejections = true; // for async handlers
// always register an error listener on streams/httpAsync Listeners
EventEmitter calls listeners synchronously, so an async listener's promise isn't awaited — emit() returns before it settles. Use once(ee, name) to await a single event as a promise. Enable captureRejections so a rejected async listener triggers the 'error' event instead of being silently lost.
const ee = new EventEmitter();
// listeners run synchronously; await doesn't pause the chain
ee.on('save', async () => {
await fs.promises.writeFile('x', 'y');
console.log('saved');
});
ee.emit('save'); // returns before the await finishes
// wait for async listeners in sequence
const { once } = require('events');
async function waitForReady() {
await once(ee, 'ready'); // promise resolves on first 'ready'
console.log('now ready');
}
// handle async errors properly
EventEmitter.captureRejections = true;
ee.on('job', async () => { throw new Error('boom'); });
ee.on('error', err => console.error(err));Event Naming & newListener
newListener and removeListener are special events emitted when listeners are added/removed — useful for hooks or profiling. setMaxListeners raises the leak-detection threshold (default 10); the warning usually means you forgot to clean up listeners. Use Symbols or constants for event names to prevent typos.
const ee = new EventEmitter();
// special events fired by EventEmitter itself
ee.on('newListener', (event, listener) => {
console.log('listener added for', event);
});
ee.on('removeListener', (event, listener) => {
console.log('listener removed for', event);
});
// limit listener count to catch leaks
ee.setMaxListeners(20);
ee.getMaxListeners(); // 20
// MaxListenersExceededWarning fires if exceeded
// convention: camelCase event names, 'error' reserved
// prefer symbols/const strings to avoid typos
const READY = Symbol('ready');
ee.emit(READY);Child Process
exec (shell, buffered)
exec() runs a command via a shell and buffers stdout/stderr — best for short commands where you want all output at once. It spawns a shell, so it expands globs and pipes. NEVER interpolate user input into the command string — that's a command-injection vulnerability; use spawn with an args array instead.
const { exec } = require('child_process');
// runs a command in a shell, buffers all output
exec('ls -la', (err, stdout, stderr) => {
if (err) { console.error('failed:', err); return; }
console.log(stdout); // full stdout as a string
console.error(stderr);
});
// promisified (Node 12+)
const { promisify } = require('util');
const execP = promisify(exec);
const { stdout } = await execP('git rev-parse HEAD');
// WARNING: shell interpolation = injection risk!
// NEVER: exec(`rm ${userInput}`)spawn (streaming, no shell)
spawn() launches a process with an args array (no shell) and streams stdout/stderr — safe and memory-efficient for large or long-running output. Because args are passed directly (not through a shell), there's no glob expansion or injection risk. Pipe child streams to other streams for pipelines.
const { spawn } = require('child_process');
// no shell by default -> safe, stream-based
const child = spawn('ls', ['-la', '/tmp']);
child.stdout.on('data', chunk => process.stdout.write(chunk));
child.stderr.on('data', chunk => process.stderr.write(chunk));
child.on('close', code => console.log('exit code', code));
child.on('error', err => console.error('failed to spawn', err));
// pipe streams directly
const gzip = spawn('gzip');
process.stdin.pipe(gzip.stdin);
gzip.stdout.pipe(process.stdout);execFile & fork
execFile runs an executable without a shell — safer than exec for running a known binary with args. fork() spawns a Node process and opens a built-in IPC channel (send/on('message')), the basis for cluster and worker_threads-like message passing between Node processes.
const { execFile, fork } = require('child_process');
// execFile: like exec but no shell (safer, slight overhead saving)
execFile('node', ['--version'], (err, stdout) => {
console.log(stdout.trim()); // v20.x.x
});
// fork: spawn another Node process with an IPC channel
const worker = fork('./worker.js');
worker.send({ task: 'compute', n: 100 });
worker.on('message', msg => console.log('result:', msg));
worker.on('exit', code => console.log('worker exited', code));
// in worker.js:
// process.on('message', msg => { process.send(result); });stdio & Arguments
stdio controls the child's standard streams: 'inherit' shares the parent's terminal (great for npm scripts), 'ignore' discards, 'pipe' captures. detached + unref() spawns a background process that survives the parent. Pass arguments as an array — spawn quotes them for you, so spaces and special chars are safe.
const { spawn } = require('child_process');
// control stdio: 'pipe' (default), 'inherit', 'ignore', or fd
const child = spawn('npm', ['test'], {
stdio: 'inherit' // child uses parent's stdin/stdout/stderr
});
// per-stream: [stdin, stdout, stderr]
spawn('cat', [], { stdio: ['pipe', process.stdout, 'ignore'] });
// set env, cwd, and detached background process
const bg = spawn('node', ['server.js'], {
cwd: '/app',
env: { ...process.env, NODE_ENV: 'production' },
detached: true // runs independently of parent
});
bg.unref(); // let parent exit without waiting
// pass arguments safely as array (no shell parsing)
spawn('echo', ['hello world']); // one arg, no quotes neededExit Codes & Signals
The 'exit' event reports code (null if killed by a signal) and signal (null for normal exit). 'close' fires after all stdio streams have closed — use it when you've piped output and need to know the streams are flushed. kill() sends a signal; SIGTERM is graceful, SIGKILL is uncatchable.
const child = spawn('sleep', ['60']);
// 'exit' fires with code + signal
child.on('exit', (code, signal) => {
if (code === 0) console.log('success');
else if (signal) console.log('killed by', signal);
else console.log('failed with code', code);
});
// send a signal to the child (SIGTERM by default)
setTimeout(() => child.kill('SIGTERM'), 1000);
child.kill(); // default SIGTERM
child.kill('SIGKILL'); // force-kill (cannot be caught)
// 'close' vs 'exit': 'close' fires after streams are closed
child.on('close', code => console.log('streams closed', code));
// check if process is still running
child.killed; // true after kill() calledCluster
Basic Clustering
cluster lets you fork multiple Node processes sharing one port, scaling across CPU cores. The primary forks workers (typically os.cpus().length). Each worker is a full Node process running the same script. Restart workers on 'exit' for resilience. Use cluster.isPrimary (formerly isMaster) to branch logic.
const cluster = require('cluster');
const http = require('http');
const os = require('os');
if (cluster.isPrimary) { // old: isMaster
// fork one worker per CPU core
for (let i = 0; i < os.cpus().length; i++) cluster.fork();
cluster.on('exit', (worker, code) => {
console.log(`worker ${worker.process.pid} died`);
cluster.fork(); // restart it
});
} else {
// workers share the same port
http.createServer((req, res) => res.end('hi'))
.listen(3000);
console.log('worker started', process.pid);
}Worker Management
The primary tracks workers via 'online' (process started), 'listening' (server bound), 'disconnect', and 'exit' events. disconnect() drains connections before killing the worker — use it for zero-downtime reloads. cluster.workers maps worker IDs to Worker objects so you can message or close them.
cluster.on('online', worker => {
console.log('worker responsive', worker.id);
});
cluster.on('listening', (worker, address) => {
console.log('worker bound to', address.port);
});
cluster.on('disconnect', worker => {
console.log('worker disconnected', worker.id);
});
// gracefully shut down a worker
const w = cluster.workers[1];
w.disconnect(); // stop accepting connections, then exit
w.send('shutdown'); // custom message
// iterate all workers
for (const id in cluster.workers) {
cluster.workers[id].send({ cmd: 'reload' });
}Shared Server Port
All workers listen on the same port; the primary load-balances incoming connections. SCHED_RR (round-robin, default on most platforms) distributes evenly; SCHED_NONE leaves it to the OS. State is NOT shared between workers — use Redis or a database for shared sessions/cache.
const cluster = require('cluster');
const http = require('http');
if (cluster.isPrimary) {
cluster.schedulingPolicy = cluster.SCHED_RR; // round-robin (default on non-Windows)
cluster.fork();
cluster.fork();
} else {
// all workers call listen(3000) — primary load-balances
http.createServer((req, res) => {
res.end(`handled by ${process.pid}`);
}).listen(3000);
}
// SCHED_RR: primary distributes connections (default)
// SCHED_NONE: OS balances (may be uneven)Worker Lifecycle & IPC
cluster.fork() opens an IPC channel (like child_process.fork): worker.send and process.on('message') exchange JSON between primary and workers. Use it to dispatch jobs or signal graceful shutdown. For CPU-heavy work also consider worker_threads, which share memory and avoid the IPC overhead.
// primary -> worker messaging
if (cluster.isPrimary) {
const worker = cluster.fork();
worker.on('message', msg => console.log('from worker:', msg));
worker.send({ task: 'heavy-compute' });
} else {
process.on('message', msg => {
console.log('from primary:', msg);
// do work, then report back
process.send({ done: true, result: 42 });
});
}
// graceful shutdown signal
process.on('message', msg => {
if (msg === 'shutdown') {
server.close(() => process.exit(0));
}
});Graceful Reload (Zero Downtime)
A rolling restart reloads workers one at a time so the app never goes down: disconnect a worker (it stops accepting new connections), wait for it to exit after draining, then fork a replacement. Trigger it with SIGHUP. Always combine with a per-worker 'shutdown' message so each worker closes its server and exits only after in-flight requests finish.
// rolling restart: reload workers one at a time
function reload() {
const workers = Object.values(cluster.workers || {});
let i = 0;
const next = () => {
if (i >= workers.length) return;
const w = workers[i++];
w.send('shutdown'); // tell worker to drain
w.disconnect(); // stop accepting connections
w.once('exit', () => {
cluster.fork(); // replacement ready
next(); // move to the next
});
};
next();
}
// trigger reload on SIGHUP
process.on('SIGHUP', reload);
// in the worker: finish in-flight requests then exit
process.on('message', msg => {
if (msg === 'shutdown') server.close(() => process.exit(0));
});Async Programming
Callbacks & Error-First Convention
Node's classic async pattern is the error-first callback: the first arg is an error (null on success), subsequent args are results. Always check err first and return early on failure. This style scales poorly with nesting — known as callback hell — so modern code uses Promises and async/await.
// Node callbacks: (err, result) => {}
const fs = require('fs');
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) return console.error('read failed', err);
console.log(data);
});
// custom error-first function
function divide(a, b, cb) {
if (b === 0) return cb(new Error('divide by zero'));
cb(null, a / b);
}
divide(10, 2, (err, result) => {
if (err) return console.error(err);
console.log(result); // 5
});Promises
A Promise represents a future value. then() handles success, catch() handles rejection, finally() runs either way. Chain then() to transform values sequentially. util.promisify converts an error-first callback function into one returning a Promise — bridging legacy APIs to async/await.
// create a promise
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve('done'), 100);
});
// consume with then/catch/finally
p.then(val => console.log(val)) // 'done'
.catch(err => console.error(err))
.finally(() => console.log('settled'));
// chain transforms
fetch(url)
.then(r => r.json())
.then(data => render(data));
// convert callback APIs
const { promisify } = require('util');
const readFile = promisify(fs.readFile);
const text = await readFile('x.txt', 'utf8');async/await
async/await makes asynchronous code look synchronous. An async function returns a Promise; await pauses until it settles. Wrap awaited calls in try/catch to handle rejections. Top-level await is allowed in ES Modules and (with a flag) in CommonJS, eliminating the need for an async IIFE wrapper.
// async functions always return a Promise
async function loadUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error('not found');
return res.json();
}
// consume with try/catch
try {
const user = await loadUser(5);
console.log(user);
} catch (err) {
console.error('failed:', err);
}
// top-level await works in ESM and CommonJS (Node 14.8+)
const config = await loadConfig();Promise.all & allSettled
Promise.all runs promises in parallel and resolves with an array of results, but rejects immediately if ANY promise rejects (fast-fail) — use when all must succeed. Promise.allSettled waits for every promise regardless of outcome, returning {status, value/reason} objects — best when partial success is acceptable.
// all: run in parallel, reject fast on first failure
const [a, b, c] = await Promise.all([
fetch('/a').then(r => r.json()),
fetch('/b').then(r => r.json()),
fetch('/c').then(r => r.json())
]);
// allSettled: wait for ALL, never rejects
const results = await Promise.allSettled([
fetch('/x'),
fetch('/y'),
fetch('/z')
]);
results.forEach(r => {
if (r.status === 'fulfilled') console.log(r.value);
else console.error(r.reason); // rejected reason
});Promise.race & Promise.any
Promise.race resolves OR rejects with the first promise to settle — useful for timeouts and racing mirrors. Promise.any resolves with the first success and only rejects (AggregateError) if every promise fails — ideal for redundant endpoints where one healthy response is enough.
// race: first to SETTLE (resolve or reject) wins
const fastest = await Promise.race([
fetch('/primary'),
fetch('/mirror'),
timeout(5000)
]);
// any: first to FULFILL wins; rejects only if ALL reject
const alive = await Promise.any([
fetch('/node1'),
fetch('/node2'),
fetch('/node3')
]);
// implement a timeout with race
function withTimeout(p, ms) {
return Promise.race([
p,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), ms))
]);
}Sequential vs Parallel
Awaiting in a loop runs sequentially (slow). Map to an array of promises and Promise.all them for parallelism (fast, but unbounded concurrency can overwhelm resources). A concurrency-limiting pool throttles parallel work — essential when calling rate-limited APIs or doing heavy I/O over thousands of items.
// SEQUENTIAL: one after another (slow)
const results = [];
for (const url of urls) {
results.push(await fetch(url).then(r => r.json()));
}
// PARALLEL: fire all, await together (fast)
const results = await Promise.all(
urls.map(u => fetch(u).then(r => r.json()))
);
// THROTTLED: limit concurrency with a pool
async function mapLimit(items, limit, fn) {
const ret = [];
const executing = [];
for (const item of items) {
const p = Promise.resolve().then(() => fn(item));
ret.push(p);
if (executing.push(p) >= limit) {
await Promise.race(executing);
executing.splice(executing.findIndex(x => x === p), 1);
}
}
return Promise.all(ret);
}Error Handling
try/catch/finally
try/catch handles synchronous errors and (with await) async rejections. finally always runs, even on return/throw — use it for cleanup like closing handles. Catch specific errors by checking err.code, and re-throw anything you don't recognize so unexpected bugs surface instead of being swallowed.
function parse(str) {
try {
return JSON.parse(str);
} catch (err) {
console.error('parse failed:', err.message);
return null;
} finally {
console.log('cleanup always runs');
}
}
// async errors need async catch
async function load() {
try {
const data = await fetchData();
return data;
} catch (err) {
if (err.code === 'ENOENT') return defaultData();
throw err; // re-throw unknown errors
}
}Error-First Callbacks
In error-first callbacks, always check err first and return early — never use data when err is set. When wrapping a callback API, forward the error to your own callback rather than throwing (throws inside callbacks can't be caught by the caller). Wrap sync code that may throw (like JSON.parse) in try/catch.
const fs = require('fs');
// ALWAYS check err first, return early
fs.readFile('missing.txt', 'utf8', (err, data) => {
if (err) {
console.error('could not read:', err.message);
return; // don't use data
}
console.log(data);
});
// propagate the error to your own caller
function readConfig(cb) {
fs.readFile('config.json', 'utf8', (err, raw) => {
if (err) return cb(err);
try { cb(null, JSON.parse(raw)); }
catch (e) { cb(e); }
});
}Uncaught Exceptions
uncaughtException fires for synchronous exceptions that escape all handlers — the process is now corrupt; log, close the server, and exit (let a process manager restart you). unhandledRejection fires for forgotten promises; since Node 15 these crash the process, so always add .catch() or await within try/catch.
// last-resort handler for sync exceptions
process.on('uncaughtException', err => {
console.error('UNCAUGHT:', err);
// the process is in an unknown state — restart it
server.close(() => process.exit(1));
});
// rejected promises nobody awaited
process.on('unhandledRejection', (reason, promise) => {
console.error('UNHANDLED REJECTION:', reason);
});
// Node 15+: unhandled rejections terminate the process
// always add .catch() or wrap in try/awaitCustom Error Classes
Subclass Error to add structured fields (code, field, status) and to distinguish error types with instanceof. Always set this.name to the class name (it isn't automatic in older targets) and call super(message). Custom errors make catch blocks readable: branch on instanceof instead of fragile string matching.
// subclass Error to add context
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.code = 'VALIDATION_FAILED';
}
}
function validate(email) {
if (!email.includes('@')) {
throw new ValidationError('email', 'invalid email');
}
}
// branch on error type with instanceof
try {
validate('no-at-sign');
} catch (err) {
if (err instanceof ValidationError) {
console.log(err.field, err.message);
} else {
throw err;
}
}Error Properties & cause
Every Error has message, name, and stack. Add a code property for stable, machine-readable handling (Node's own errors use codes like ENOENT, EACCES). The ES2022 cause option (Node 16.9+) chains an underlying error so you can wrap low-level failures while preserving the original stack for debugging.
// built-in Error props
const e = new Error('something broke');
e.message; // 'something broke'
e.stack; // stack trace string
e.name; // 'Error'
// add a code for machine-readable handling
const err = new Error('file missing');
err.code = 'ENOENT';
err.errno = -2;
// Error.cause (Node 16.9+) — chain underlying errors
try {
JSON.parse(bad);
} catch (original) {
throw new Error('config parse failed', { cause: original });
}
// inspect the cause
console.log(err.cause); // the original SyntaxErrorDomains & Cleanup
Use 'beforeExit' for async cleanup (it can postpone exit), and 'exit' only for synchronous work. Handle SIGTERM (container shutdown) and SIGINT (Ctrl-C) to drain connections before exiting — add a force-exit timeout so a stuck connection can't hang the process. Domains are deprecated; use try/catch and async error handling instead.
// modern cleanup: 'beforeExit' for async, 'exit' for sync
process.on('beforeExit', async () => {
await closeDbConnections();
});
process.on('exit', code => {
// ONLY sync code allowed here — no async, no promises
console.log('exiting with', code);
});
// graceful signal handling
const shutdown = sig => {
console.log('got', sig);
server.close(() => process.exit(0));
setTimeout(() => process.exit(1), 5000).unref(); // force exit
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));Debugging
console Methods
console offers more than log: table renders arrays of objects, time/timeEnd measures duration, dir dumps an object with configurable depth, trace prints a stack, and count tallies hits. Use console.error (stderr) so logs and errors can be separated in production pipelines.
console.log('basic'); // stdout
console.error('oops'); // stderr
console.warn('careful'); // stderr
// formatted table
console.table([{ id: 1, n: 'A' }, { id: 2, n: 'B' }]);
// inspect an object in depth
console.dir(obj, { depth: null, colors: true });
// timing a block
console.time('loop');
for (let i = 0; i < 1e6; i++) {}
console.timeEnd('loop'); // loop: 3.2ms
// stack trace
console.trace('where am I');
// count calls
for (let i = 0; i < 3; i++) console.count('hit');node inspect & DevTools
--inspect starts the V8 inspector so Chrome DevTools (chrome://inspect) or VS Code can attach. --inspect-brk pauses on the first line so you can set breakpoints before startup code runs. The classic 'node inspect' is a terminal debugger useful over SSH where a browser isn't available.
# open Chrome DevTools debugger
node --inspect app.js
# visit chrome://inspect, click "inspect"
# pause on first line (break before anything runs)
node --inspect-brk app.js
# wait for a debugger client to attach
node --inspect-wait app.js
# the legacy CLI debugger (still works, no browser)
node inspect app.js
# > cont, step, next, breakpoints, replThe debugger Statement
The debugger statement pauses execution ONLY when a debugger (DevTools or VS Code) is attached — otherwise it's a no-op, safe to leave in code. Combine with --inspect-brk to stop at startup. For ad-hoc inspection, drop in debugger; inside loops or conditions instead of setting UI breakpoints.
function findBug(arr) {
let sum = 0;
for (const n of arr) {
debugger; // pauses only when a debugger is attached
sum += n;
}
return sum;
}
// run with --inspect-brk to hit the breakpoint
// node --inspect-brk app.js
// conditional breakpoints in DevTools/VSCode:
// if (n > 100) { debugger; }VS Code Debugging
VS Code's Node debugger (F5) reads .vscode/launch.json. "request": "launch" runs the program with the inspector built in; "attach" connects to a process already started with --inspect. skipFiles hides Node internals from stepping. Use the "restart" flag or nodemon to auto-relaunch on file changes.
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug App",
"program": "${workspaceFolder}/app.js",
"skipFiles": ["<node_internals>/**"]
},
{
"type": "node",
"request": "attach",
"name": "Attach by PID",
"processId": "${command:PickProcess}"
}
]
}
// F5 to start; set breakpoints in the editor
// "restart": true, "nodemon" for auto-restart on changeWarnings & Stack Traces
--trace-warnings prints stacks for process warnings (memory leaks, deprecated APIs). --trace-deprecation surfaces legacy-API usage; --throw-deprecation turns them into hard errors for stricter CI. --cpu-prof writes a profile you can open in Chrome DevTools to find hot functions. Raise Error.stackTraceLimit for deeper async traces.
# show stack traces for process warnings
node --trace-warnings app.js
# deprecation warnings (use --throw-deprecation to make them errors)
node --trace-deprecation app.js
# capture a stack trace manually
const e = new Error('here');
console.log(e.stack);
# show async stack traces (Node 12+ enabled by default)
Error.stackTraceLimit = 50; // deeper traces
# profile CPU usage
node --cpu-prof app.js # writes CPU.*.cpuprofile
# then load the file in Chrome DevToolsCrypto
Hashing (createHash)
createHash produces a one-way digest. SHA-256 is the modern default for checksums and signatures; avoid MD5/SHA-1 for security (collision-prone). You can feed data incrementally and stream large files without buffering. digest('hex') returns a hex string; 'base64' is also common. Hashing is NOT password storage — use pbkdf2/scrypt.
const crypto = require('crypto');
// common hashes: sha256, sha512, md5 (insecure!), sha1
const hash = crypto.createHash('sha256');
hash.update('hello');
hash.update(' world'); // can chain updates
console.log(hash.digest('hex'));
// b94d... (64 hex chars)
// one-shot hashing
const h = crypto.createHash('sha256').update('data').digest('hex');
console.log(h);
// file hash (streaming)
const fs = require('fs');
const fhash = crypto.createHash('sha256');
fs.createReadStream('big.iso').on('data', c => fhash.update(c))
.on('end', () => console.log(fhash.digest('hex')));HMAC (createHmac)
HMAC signs data with a shared secret so the receiver can verify both integrity and authenticity. It's the basis of JWT signatures and webhook verification (GitHub, Stripe). Always compare signatures with timingSafeEqual — a normal === leaks information through timing differences that attackers can exploit.
const crypto = require('crypto');
// HMAC = keyed hash (authenticity + integrity)
const secret = process.env.SECRET_KEY;
const hmac = crypto.createHmac('sha256', secret);
hmac.update('payload to sign');
const signature = hmac.digest('hex');
// verify a webhook signature (constant-time compare)
function verify(payload, receivedSig) {
const expected = crypto.createHmac('sha256', secret)
.update(payload).digest('hex');
// timingSafeEqual prevents timing attacks
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(receivedSig)
);
}Random Bytes
crypto.randomBytes produces cryptographically secure random data — use it for tokens, IDs, and secrets. The async form avoids blocking the event loop when generating many bytes. crypto.randomInt gives unbiased random integers, and randomUUID() generates RFC-4122 v4 UUIDs. Never use Math.random() for anything security-related.
const crypto = require('crypto');
// async (preferred — doesn't block the event loop)
crypto.randomBytes(16, (err, buf) => {
const token = buf.toString('hex'); // 32-char hex
});
// sync (blocks; fine for small sizes)
const id = crypto.randomBytes(8).toString('hex');
// random integer in [min, max)
const n = crypto.randomInt(1, 101); // 1..100
// random UUID (Node 14.17+)
const uuid = crypto.randomUUID();
// '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'
// use crypto, NEVER Math.random() for secrets!Encryption (createCipheriv)
AES-256-GCM is the recommended symmetric cipher — it's authenticated, so tampering is detected. The IV (nonce) must be unique per encryption with a given key (never reuse); store it alongside the ciphertext. setAuthTag verifies integrity on decrypt. Never use the deprecated createCipher (it derives keys insecurely) — always use createCipheriv with a proper key.
const crypto = require('crypto');
const algorithm = 'aes-256-gcm';
// AES-256-GCM (authenticated encryption) ----------------
const key = crypto.randomBytes(32); // 256-bit key
const iv = crypto.randomBytes(12); // 96-bit nonce
function encrypt(text) {
const cipher = crypto.createCipheriv(algorithm, key, iv);
const enc = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag(); // GCM auth tag
return { enc, tag };
}
function decrypt(enc, tag) {
const decipher = crypto.createDecipheriv(algorithm, key, iv);
decipher.setAuthTag(tag); // verify integrity
return Buffer.concat([decipher.update(enc), decipher.final()]).toString('utf8');
}Password Hashing (pbkdf2, scrypt)
Hash passwords with a slow, salted KDF — never with a plain hash. pbkdf2 (tune iterations upward as hardware improves) and scrypt (memory-hard, GPU-resistant) are built into Node. Store iterations, salt, and hash together so you can verify and later upgrade parameters. Use timingSafeEqual to check hashes without leaking timing info.
const crypto = require('crypto');
// PBKDF2: tuned with iterations
function hashPbkdf2(password) {
const salt = crypto.randomBytes(16);
const iterations = 100000;
const hash = crypto.pbkdf2Sync(password, salt, iterations, 64, 'sha256');
return `${iterations}.${salt.toString('hex')}.${hash.toString('hex')}`;
}
// scrypt: memory-hard, resistant to GPU/ASIC attacks
function hashScrypt(password) {
const salt = crypto.randomBytes(16);
const hash = crypto.scryptSync(password, salt, 64, { N: 16384 });
return salt.toString('hex') + '.' + hash.toString('hex');
}
// verify: re-hash with the same salt and compare
function verify(password, stored) {
const [iter, salt, hash] = stored.split('.');
const computed = crypto.pbkdf2Sync(password, Buffer.from(salt, 'hex'),
+iter, 64, 'sha256');
return crypto.timingSafeEqual(Buffer.from(hash, 'hex'), computed);
}Timing-Safe Compare
timingSafeEqual compares two buffers in constant time, preventing timing attacks where an attacker infers a secret byte-by-byte from response delays. Both buffers must be the same length (check first, but that length check itself can leak length — for secrets, hash both sides first). Use it whenever comparing tokens, signatures, or API keys.
const crypto = require('crypto');
// NORMAL compare leaks via timing (returns early on first diff)
// 'secret' === userInput // BAD for secrets
// timingSafeEqual takes equal-length Buffers
function safeEqual(a, b) {
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
if (bufA.length !== bufB.length) return false;
return crypto.timingSafeEqual(bufA, bufB);
}
// common use: API key check
function auth(req) {
const provided = req.headers['x-api-key'] || '';
return safeEqual(provided, process.env.API_KEY);
}OS Module
System Information
os exposes the host environment. platform/arch/type identify the OS for portability checks. os.EOL gives the platform's end-of-line sequence — use it when writing platform-correct text files. hostname is the machine's network name, useful for logging which instance handled a request.
const os = require('os');
os.platform(); // 'linux', 'darwin', 'win32'
os.arch(); // 'x64', 'arm64'
os.type(); // 'Linux', 'Darwin', 'Windows_NT'
os.release(); // kernel version
os.hostname(); // machine hostname
os.uptime(); // seconds since boot
os.EOL; // '\n' (POSIX) or '\r\n' (Windows)
// detect platform portably
const isWin = os.platform() === 'win32';
const isMac = os.platform() === 'darwin';CPU Info
os.cpus() returns an array of logical cores (hyperthreads included), so its length is the right concurrency hint for cluster.fork. Each entry has model, speed, and times (user/sys/idle). loadavg reports system load (Linux/macOS only) — a value above cpus().length suggests saturation.
const os = require('os');
// logical core count (threads, not physical)
os.cpus().length; // 8
// detailed per-core info
os.cpus().forEach((cpu, i) => {
console.log(`core ${i}: ${cpu.model} @ ${cpu.speed}MHz`);
});
// 1/5/15-minute load averages (NOT on Windows)
os.loadavg(); // [0.45, 0.32, 0.28]
// set process scheduling priority
os.setPriority(os.constants.priority.PRIORITY_BELOW_NORMAL);Memory
os.totalmem/freemem report system RAM, while process.memoryUsage reports this process's footprint. rss is the OS-level memory; heapUsed is the live JS objects. Watch heapUsed for leaks. To convert bytes to MB, divide by 1024*1024 (1048576). Tune --max-old-space-size when heap grows near the limit.
const os = require('os');
// system memory (bytes)
os.totalmem(); // e.g. 17179869184
os.freemem(); // currently free bytes
// process memory usage (bytes)
const m = process.memoryUsage();
m.rss; // resident set size (total RAM held)
m.heapTotal; // V8 heap allocated
m.heapUsed; // V8 heap actually used
m.external; // C++ objects bound to JS
m.arrayBuffers; // memory in ArrayBuffers
// human-readable
console.log(`heap: ${(m.heapUsed / 1048576).toFixed(1)} MB`);Network Interfaces
os.networkInterfaces() lists each network interface with its addresses, MAC, and family (IPv4/IPv6). The 'internal' flag marks loopback. It's the portable way to discover the machine's LAN IP for binding servers or logging. Each interface can have multiple addresses (multihomed hosts).
const os = require('os');
const nets = os.networkInterfaces();
for (const name of Object.keys(nets)) {
for (const net of nets[name]) {
console.log(`${name} ${net.family}: ${net.address} (${net.mac})`);
}
}
// en0 IPv4: 192.168.1.10 (aa:bb:cc:...)
// lo IPv4: 127.0.0.1 (:::...)
// find this machine's LAN IPv4
function getLanIp() {
for (const nets of Object.values(os.networkInterfaces())) {
for (const n of nets) {
if (n.family === 'IPv4' && !n.internal) return n.address;
}
}
return '127.0.0.1';
}User & Paths
os.homedir returns the user's home directory and os.tmpdir the system temp folder — both platform-aware, so prefer them over hard-coded paths. os.userInfo gives the current user's identity. os.constants centralizes numeric signal/errno/priority values so you don't hardcode magic numbers like 15 for SIGTERM.
const os = require('os');
os.homedir(); // /home/alice or C:\Users\alice
os.tmpdir(); // /tmp or C:\Users\...\Temp
os.userInfo(); // { username, uid, gid, homedir, shell }
os.constants; // signal/errno/priority constants
// errno constants map
os.constants.errno.ENOENT; // -2
// signal constants
os.constants.signals.SIGTERM; // 15
// priority constants
os.constants.priority.PRIORITY_HIGH;
// build a portable temp path
const path = require('path');
const tmpFile = path.join(os.tmpdir(), 'app-' + Date.now() + '.log');Process
process.argv
process.argv holds command-line arguments: argv[0] is the node binary, argv[1] is the script, and the rest are user args. Slice off the first two to get just the user args. For anything beyond toy scripts, use a parser like minimist, yargs, or commander — they handle flags, types, defaults, and help text.
// argv[0] = node path, argv[1] = script path, argv[2+] = args
// node app.js --port 3000 users.csv
console.log(process.argv);
// ['/usr/bin/node', '/app/app.js', '--port', '3000', 'users.csv']
const args = process.argv.slice(2);
console.log(args); // ['--port', '3000', 'users.csv']
// minimal flag parsing
function parseArgs(arr) {
const out = { _: [] };
for (let i = 0; i < arr.length; i++) {
if (arr[i].startsWith('--')) out[arr[i].slice(2)] = arr[++i];
else out._.push(arr[i]);
}
return out;
}
// for real CLI use commander, yargs, or minimistprocess.env
process.env holds environment variables as strings. Conventionally PORT, NODE_ENV, DATABASE_URL, and API_KEY come from env so the same code runs across environments. Use the dotenv package to load a .env file in development. Never commit secrets — inject them via the host/CI environment.
// read environment variables
const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;
const nodeEnv = process.env.NODE_ENV || 'development';
const isProd = process.env.NODE_ENV === 'production';
// set an env var (affects child processes too)
process.env.MY_VAR = 'value';
// load .env files (install: npm i dotenv)
// require('dotenv').config();
// enumerate everything
for (const [k, v] of Object.entries(process.env)) {
console.log(k, '=', v);
}process.exit & Exit Codes
process.exit(n) terminates immediately, possibly before I/O buffers flush — prefer setting process.exitCode and letting the event loop drain naturally. Exit 0 means success; non-zero signals failure to shells and CI. The convention 128+N encodes termination by signal N (so SIGTERM yields 143).
// exit immediately with a status code
process.exit(0); // success
process.exit(1); // general failure
// exit code is also settable
process.exitCode = 2; // used if the loop ends naturally
// prefer setting exitCode + returning over process.exit()
// so async work can finish
// common conventions:
// 0 success
// 1 general error
// 2 misuse (bad args)
// 124 timeout (from the 'timeout' command)
// 128+N killed by signal N (e.g. 143 = SIGTERM)stdin / stdout / stderr
process.stdout and process.stderr are writable streams — write() doesn't add a newline (unlike console.log). stdin is a readable stream you can consume with 'data' events or async iteration (the cleanest modern pattern). Use stderr for diagnostics so it can be separated from real output in pipes.
// stdout/stderr are writable streams
process.stdout.write('no newline');
process.stdout.write('line\n');
console.log === process.stdout.write.bind(...); // roughly
// stdin is a readable stream
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => console.log('got:', chunk));
// read all of stdin (async iteration)
async function readStdin() {
let data = '';
for await (const chunk of process.stdin) data += chunk;
return data;
}
// ask a yes/no question
process.stdin.resume();
process.stdin.once('data', input => process.exit(input[0] === 'y'.charCodeAt(0) ? 0 : 1));Process Events (Signals)
'exit' fires after the event loop empties — only synchronous code runs there. 'beforeExit' can run async work and may postpone exit. Handle SIGINT (Ctrl-C) and SIGTERM (docker stop, kill) to close servers and DB connections gracefully. Always set a force-exit timeout so a hung connection can't trap the process forever.
// lifecycle events
process.on('exit', code => {
// sync only here — no async, no event loop
console.log('bye, code', code);
});
process.on('beforeExit', () => {
// async OK; can postpone exit
});
// signal handlers (Unix + Windows for SIGINT/SIGTERM)
process.on('SIGINT', () => { // Ctrl-C
console.log('graceful shutdown...');
server.close(() => process.exit(0));
});
process.on('SIGTERM', () => { // kill / docker stop
server.close(() => process.exit(0));
});
process.on('SIGHUP', () => reloadConfig());memoryUsage & uptime
process exposes identity (pid, ppid, platform, version) and runtime stats (uptime, memoryUsage, cpuUsage). cwd() is the working directory — chdir changes it, affecting relative path resolution. memoryUsage and cpuUsage are the basis for health-check endpoints and autoscaling metrics.
// process info
process.pid; // OS process ID
process.ppid; // parent PID
process.platform; // 'linux', 'darwin', 'win32'
process.version; // 'v20.11.0'
process.versions; // { v8, openssl, modules, ... }
process.cwd(); // current working directory
process.uptime(); // seconds since process started
// change directory
process.chdir('/tmp');
// memory snapshot
const m = process.memoryUsage();
console.log(m.rss, m.heapUsed);
// CPU usage of this process
const cpu = process.cpuUsage();
console.log(cpu.user, cpu.system); // microsecondsNetwork (net)
TCP Server
net.createServer makes a raw TCP server. Each connection invokes the callback with a Socket (a Duplex stream). Read with 'data' events, write with socket.write. Always handle 'error' on sockets or the process crashes. TCP is the foundation under HTTP — useful for custom protocols, games, and proxies.
const net = require('net');
const server = net.createServer(socket => {
console.log('client connected', socket.remoteAddress);
socket.write('welcome\n');
socket.on('data', data => {
socket.write('echo: ' + data); // echo back
});
socket.on('end', () => console.log('client left'));
socket.on('error', err => console.error('socket err', err));
});
server.listen(5000, () => {
console.log('TCP server on port 5000');
});
// test with: nc localhost 5000 or telnet localhost 5000TCP Client
net.connect (alias createConnection) opens a TCP client socket. The callback fires on connect; 'data' events deliver incoming bytes. Set an encoding to receive strings instead of Buffers. Like any stream, handle 'error' to avoid crashes. Sockets auto-close on 'end'; call client.end() to half-close your side.
const net = require('net');
// connect to a TCP server
const client = net.connect(5000, 'localhost', () => {
console.log('connected');
client.write('hello server\n');
});
client.setEncoding('utf8');
client.on('data', data => console.log('server said:', data));
client.on('end', () => console.log('disconnected'));
client.on('error', err => console.error(err));
// or with an options object + timeout
const c = net.createConnection({
port: 5000,
host: 'localhost',
timeout: 5000
});Socket Events & Data
TCP is a byte stream, not a message protocol — one 'data' event doesn't equal one message; data can be split or coalesced. You MUST frame messages yourself (length prefixes, delimiters, or a parser). setKeepAlive detects dead peers, setTimeout kills idle connections, and 'drain' signals the write buffer emptied.
const net = require('net');
net.createServer(socket => {
socket.setEncoding('utf8');
socket.setKeepAlive(true, 30000); // enable TCP keepalive
socket.setTimeout(60000); // idle timeout
socket.on('data', chunk => {
// TCP is a STREAM: chunks may split or merge messages.
// Frame your protocol (length prefix, delimiter, etc.)
console.log('received', chunk.length, 'bytes');
});
socket.on('timeout', () => socket.end());
socket.on('close', hadError => console.log('closed', hadError));
socket.on('drain', () => console.log('buffer drained'));
}).listen(5000);Writing & Half-close
socket.write respects backpressure (returns false when full — wait for 'drain'). end() half-closes: it flushes pending writes then sends FIN while still reading the peer. destroy() kills the socket immediately. Use remoteAddress/remotePort for logging and access control. Pause/resume throttle a fast sender.
const net = require('net');
net.createServer(socket => {
// write() returns false when the buffer is full (backpressure)
const ok = socket.write(bigPayload);
if (!ok) socket.once('drain', writeMore);
// half-close: send FIN, keep reading
socket.end(); // writes any pending data, then closes our side
// socket.end('final message\n'); // write + close
// fully destroy the socket immediately
socket.destroy();
// pause/resume reading
socket.pause();
socket.resume();
// address info
socket.remoteAddress; // '192.168.1.5'
socket.remotePort; // 54321
}).listen(5000);Server Events & IPC
A net.Server emits 'connection' (per client), 'listening', 'error' (handle EADDRINUSE), and 'close'. listen can take a path for a Unix socket (or named pipe on Windows) — fast, secure local IPC between processes on the same host. server.close() stops accepting new connections and waits for existing ones to finish.
const net = require('net');
const server = net.createServer();
server.on('connection', socket => {}); // same as createServer cb
server.on('listening', () => console.log('bound'));
server.on('error', err => {
if (err.code === 'EADDRINUSE') console.error('port taken');
});
server.on('close', () => console.log('server closed'));
server.listen(5000);
// Unix domain socket / named pipe (fast local IPC)
server.listen('/tmp/app.sock');
// Windows: server.listen('\\\\.\\pipe\\app');
// stop accepting, close idle connections
server.close(() => console.log('all done'));
// get bound address
server.address(); // { port: 5000, family: 'IPv4', address: '::' }Timers & Utilities
Timers (setTimeout, setInterval)
setTimeout runs once after a delay, setInterval repeats. setImmediate runs after I/O events in the same loop turn. queueMicrotask runs even sooner, right after the current operation. Ordering within a loop is: microtasks first, then timers, then I/O, then setImmediate — useful to know when scheduling follow-up work.
// run once after a delay (ms)
const t = setTimeout(() => console.log('hi'), 1000);
// run repeatedly every interval
const i = setInterval(() => console.log('tick'), 2000);
// run right after the current event loop turn
setImmediate(() => console.log('immediate'));
// run before the next event loop turn (microtask)
queueMicrotask(() => console.log('microtask'));
// order inside one loop:
// microtasks -> timers -> I/O -> check (setImmediate)Clearing Timers & ref
Each timer has a matching clear function. unref() marks a timer as 'don't keep the process alive' — if only unref'd timers remain, Node exits (handy for periodic cleanup that shouldn't trap the process). ref() reverses it. Extra arguments after the delay are forwarded to the callback, avoiding an extra arrow function.
const t = setTimeout(() => {}, 1000);
clearTimeout(t); // cancel
const i = setInterval(() => {}, 1000);
clearInterval(i);
const imm = setImmediate(() => {});
clearImmediate(imm);
// keep the event loop alive?
// timers do by default; unref() lets the process exit if
// only this timer is pending
const timer = setInterval(() => {}, 1000);
timer.unref(); // won't keep Node alive
timer.ref(); // keep alive again
// pass args to the callback
setTimeout((a, b) => console.log(a, b), 500, 'x', 'y');util.inspect & format
util.format builds strings with printf-style placeholders (%s %d %j %o %O %%). util.inspect renders any object — pass { depth: null, colors: true } for a full colored dump, useful in logs. util.deprecate wraps a function to emit a DeprecationWarning (surfaced with --trace-deprecation) when called.
const util = require('util');
// format like printf
util.format('%s:%d', 'port', 3000); // 'port:3000'
util.format('%j', { a: 1 }); // JSON
// deep inspect an object (what console.dir uses)
util.inspect(obj, { depth: null, colors: true, compact: false });
// pretty-print a function (its source)
util.inspect(function f() { return 1; });
// deprecate an API with a warning
const oldFn = util.deprecate(
() => doThingOldWay(),
'oldFn() is deprecated, use newFn() instead'
);util.promisify
util.promisify wraps an error-first-callback function into one returning a Promise — the bridge from legacy callback APIs to async/await. Many core modules now ship native promise versions (fs/promises) which are preferable, but promisify still rescues third-party callback APIs. A function can define [util.promisify.custom] to control the conversion.
const util = require('util');
const fs = require('fs');
// turn an error-first callback fn into a promise fn
const readFile = util.promisify(fs.readFile);
const text = await readFile('data.txt', 'utf8');
// works on custom functions too
function delay(ms, cb) { setTimeout(() => cb(null, ms), ms); }
const delayP = util.promisify(delay);
await delayP(100);
// custom promisify symbol (controls the promise behavior)
// const { promisify } = util;
// obj[util.promisify.custom] = () => Promise.resolve(42);
// many core modules ship promise versions directly:
// require('fs/promises'), require('dns/promises')util types & callbackify
util.types offers reliable type checks (isPromise, isAsyncFunction, isRegExp, isMap) that survive cross-realm and subclass edge cases better than instanceof. util.callbackify reverses promisify — useful when you must expose a promise-based function to a callback-style API. util.MIMEType (Node 19+) parses and manipulates MIME types.
const util = require('util');
// type checks
util.isPromise(Promise.resolve()); // true
util.isFunction(() => {}); // true
util.types.isMap(new Map()); // true
util.types.isAsyncFunction(async () => {}); // true
util.types.isRegExp(/x/); // true
// reverse of promisify: promise -> error-first callback
const readFileCb = util.callbackify(require('fs/promises').readFile);
readFileCb('x.txt', 'utf8', (err, data) => {
if (err) console.error(err);
else console.log(data);
});
// MIME type utilities (Node 19+)
util.MIMEType.parse('text/html; charset=utf-8');nextTick vs setImmediate
process.nextTick runs right after the current synchronous operation, BEFORE I/O — so recursively calling nextTick can starve I/O indefinitely (the loop never advances). setImmediate runs after I/O, in the 'check' phase. To yield to the event loop and let pending I/O settle, prefer setImmediate; reserve nextTick for cleanup that must happen before any I/O callback.
// process.nextTick: runs BEFORE I/O, after the current op
process.nextTick(() => console.log('tick'));
// setImmediate: runs AFTER I/O events, in the 'check' phase
setImmediate(() => console.log('immediate'));
// setTimeout(0): runs in the timers phase, after nextTick
setTimeout(() => console.log('timeout'), 0);
// typical order in one loop turn:
// 1. current sync code
// 2. nextTick callbacks (can starve I/O!)
// 3. timers
// 4. I/O callbacks
// 5. setImmediate
// prefer setImmediate to yield to I/O; use nextTick for
// immediate post-sync cleanupFragmentos de Node.js relacionados
Copy-paste ready code for common tasks.
fs Module
Read, write, and watch files with promises and callbacks.
HTTP Server
Build an HTTP server with the http module and routing.
Streams
Pipe, transform, and consume streams efficiently.
EventEmitter
Emit and listen for custom events.
path Module
Join, resolve, and parse file paths cross-platform.
Buffers
Work with binary data using Buffer and TypedArrays.
Child Process
Spawn, exec, and fork external processes.
Async Patterns
Run promises in parallel, sequentially, and with limits.
Was this helpful?