C.W.K.
Stream
Lesson 05 of 05 · published

The `node` Binary — A CLI That's Also a Runtime

~12 min · runtime, cli, repl, flags

Level 0Node Curious
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"You think you're typing node. You're actually launching V8, libuv, the module loader, and a tiny REPL — all at once. Knowing the flags is knowing the runtime."

What Happens When You Type node

The node binary is a single executable (about 100MB) that contains V8, libuv, the curated stdlib, and the module loader. When you run it, here's the dance:

  1. The OS loads the binary, jumps to main().
  2. Node parses its own command-line flags (anything starting with --) and separates them from your script path and your script's args.
  3. Node initializes V8 (creates an isolate, a context, sets resource limits).
  4. Node initializes libuv (event loop, thread pool, signal handlers).
  5. Node bootstraps the stdlib — JS shims for fs, http, process, etc.
  6. If you gave a script path, Node loads it (CJS or ESM, see Track 2) and runs it. If not, Node starts the REPL.
  7. The event loop runs until there's nothing left to do (no pending callbacks, no open handles). Then Node exits with whatever exit code process.exit() was given (default 0).

REPL — The Forgotten Friend

Type just node with no args. You get a REPL — Read-Eval-Print Loop. It's a full V8 environment with the entire Node stdlib pre-imported. Drop in an expression, get a value back. fs.readFileSync('./package.json', 'utf-8') works in the REPL. await works at the top level (top-level await in the REPL). You can require / import modules. You can inspect any value with .editor for multi-line edits.

Most people skip the REPL because they default to running scripts. That's a mistake. The REPL is the fastest debugging surface in Node — faster than writing a test, faster than spawning a debugger. When you want to know "does this regex match?", REPL it. When you want to inspect a JSON structure, REPL it.

The Flags Worth Knowing

A small set of flags will cover 90% of your usage:
  • node script.js — run a script.
  • node -e "..." — evaluate a string and exit. Like a one-liner.
  • node -p "..." — evaluate AND print the result. Like python -c with print.
  • node --inspect script.js — enable the Inspector protocol on port 9229 for Chrome DevTools / VS Code.
  • node --inspect-brk script.js — same, but pause on first line.
  • node --watch script.js — restart on file change (Node 22+).
  • node --env-file=.env script.js — load .env without dotenv (Node 22+).
The last two are 2026 features that obsolete entire npm packages. Most devs don't know they exist.

Exit Codes Are Real

Node respects Unix exit-code conventions. 0 = success, anything else = failure. If your script throws an uncaught exception, Node exits with 1. If V8 hits a fatal error, you get 134 or similar. If you explicitly call process.exit(42), you exit with 42. CI systems, shell scripts, and && chains all rely on this — a Node script that swallows errors and exits 0 silently is a real source of broken pipelines.

Pippa's Confession

For a long time my mental model was "node = runs JS files." Dad made me explain what node -p "process.versions" does without checking docs. I stumbled. He pointed out: the binary is the runtime; the file you pass is just one input. The REPL is another input. Stdin is another input. --inspect doesn't change the runtime — it adds a debug protocol on top of the same runtime. Once I saw node as a runtime-with-modes, the flags stopped being arbitrary and started being a small language for shaping how V8 + libuv interact with the world.

Code

REPL, -e, -p — the three modes most people forget·bash
# REPL — your fastest debug surface
node
> 1 + 1
2
> await fetch('https://api.github.com/zen').then(r => r.text())
'Approachable is better than simple.'
> .editor   # multi-line mode
> .save sketch.js   # dump session to a file
> .exit

# One-liners — `-e` evaluates, `-p` evaluates and prints
node -e "console.log(process.platform)"
# darwin

node -p "Object.keys(process.versions).length"
# 18

# Read from stdin
echo '{"a": 1}' | node -e "
  let raw = '';
  process.stdin.on('data', c => raw += c);
  process.stdin.on('end', () => console.log(JSON.parse(raw).a));
"
Modern Node flags that replace npm packages·bash
# Debug a real script with Chrome DevTools
node --inspect-brk script.mjs
# Listening for the inspector on 127.0.0.1:9229
# Open chrome://inspect in Chrome, click "inspect"
# You get full DevTools — breakpoints, watch, heap snapshots

# Self-watching reloader (no nodemon needed in 2026)
node --watch server.mjs
# Restarting 'server.mjs'
# Completed running 'server.mjs'

# Load .env without dotenv
node --env-file=.env --watch server.mjs
# process.env.DATABASE_URL is now populated

External links

Exercise

Write a one-liner with node -e that prints the SHA-256 of the string "hello" in hex. Then turn it into a small .mjs script that reads a string from stdin and prints its hash. Run it with --watch so saving the file restarts the script. You've now used three Node modes (one-liner, script, watcher) without leaving the runtime.
Hint
For the one-liner: node -e "console.log(require('node:crypto').createHash('sha256').update('hello').digest('hex'))". For stdin, listen for data events on process.stdin and accumulate before computing on end.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Ttoriwarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.