19 — The Node.js runtime¶
One line: Node is the language plus an operating-system process, and most of its surprises are the operating system showing through: arguments and environment as strings, files as bytes, pipes that buffer, and exit statuses.
This chapter covers what Node adds around the language. It starts with the process itself — arguments, environment, exit codes — then files, paths and file URLs, and standard input and output. Bytes come next with Buffer, then streams, which move bytes in pieces and push back when the reader is slow. EventEmitter follows, because streams and servers are built on it, then an HTTP server with fetch as its client, and last the test runner that ships with Node.
The pages¶
| Page | Level | In one line | Status |
|---|---|---|---|
process |
101 | A script's first argument is process.argv[2], after the Node binary and the script path; every process.env value is a string, so process.env.DEBUG = false stores "false"; and process.exitCode = 3 exits with 3 only after pending work finishes. |
stub |
| Files | 101 | readFile returns a Buffer unless you name an encoding, and with "utf8" it neither strips a byte order mark nor reports bad bytes: EF BB BF stays as U+FEFF at the start of the string, and a stray FF becomes U+FFFD. |
stub |
| Paths and file URLs | 101 | A relative path is resolved against process.cwd(), not the module's folder; import.meta.dirname names that folder, and new URL(import.meta.url).pathname is not a usable path: a space in it stays %20 until fileURLToPath decodes it. |
stub |
| stdin, stdout and pipes | 201 | When stdout is a pipe, process.stdout.isTTY is undefined, not false, and writes are asynchronous: process.exit() right after writing 4 MiB lets only a fraction through (64 to 128 KiB in these runs), while setting process.exitCode delivers all of it. |
stub |
Buffer |
201 | Buffer is a Uint8Array subclass that breaks two of its habits: buf.slice() shares memory where Uint8Array's slice copies, and Buffer.from("hello").buffer is a shared pool of Buffer.poolSize bytes, not five. |
stub |
| Streams | 301 | write() returning false is backpressure: the writable's buffer has reached its highWaterMark, 64 KiB by default (16 KiB on Windows). Nothing stops you writing anyway — memory just grows — unless you wait for drain or let pipeline wait for you. |
stub |
EventEmitter |
201 | emit runs every listener synchronously, in the order added, before it returns; and an 'error' event with no listener is not ignored like other events: emit throws it, and if nothing catches it the process exits with status 1. |
stub |
| HTTP | 201 | fetch rejects only when no response arrives: a 404 from the server resolves normally with response.ok set to false, while a refused connection rejects with TypeError: fetch failed, the real reason kept in error.cause. |
stub |
| The built-in test runner | 201 | node --test needs no package: it finds *.test.mjs, *.test.ts and anything under test/, runs each file in its own child process, and exits with status 1 when one test fails — here assert.equal(0.1 + 0.2, 0.3). |
stub |
Boundaries¶
Workers, child processes and shared memory are in Workers and parallelism; the event loop and timers are in Async and the event loop; import.meta and module loading are in Modules.