Starting out all 3 written
What Node actually is, how to run a file, and how to read the error you are certainly about to get.
1 What Node actually is, and what it is not
Node is a program that runs JavaScript on your own computer instead of inside a web page. That is the whole idea. The rest of this course is what becomes possible once JavaScript can read files, start other programs and answer network requests.
Your first program
Every panel on this site is a real file that was run by a real node when the
page was built. The file is shown with its name, the command underneath it is exactly what was
typed, and the output is exactly what came back.
example.mjs
console.log("Hello from Node.");
console.log("Running on Node", process.version);
check("this is Node 26", process.version.startsWith("v26."), true);
What node printed, when the build ran it
node example.mjs
Hello from Node. Running on Node v26.7.0 ok this is Node 26
The last line is a check. check(name, got, want) prints
ok when the two values are equal and FAIL when they are not, and a single
FAIL anywhere stops this site from being built at all. It is how every claim on these
pages is held to account. The next lesson shows where it comes from.
Two pieces: an engine, and everything around it
Node is built from two main parts. V8 is the JavaScript engine from Google Chrome: it turns your code into machine instructions and runs it. libuv is a library that talks to the operating system on Node's behalf — files, network, timers — and it is what lets one JavaScript program wait on many of those at once.
example.mjs
const { node, v8, uv, openssl } = process.versions;
console.log("node ", node);
console.log("v8 ", v8, " <- the engine Chrome uses");
console.log("libuv ", uv, " <- the part that talks to the operating system");
console.log("openssl", openssl, " <- encryption, for https and crypto");
check("Node reports the V8 it was built with", typeof v8, "string");
check("and the libuv underneath it", uv.split(".").length, 3);
What node printed, when the build ran it
node example.mjs
node 26.7.0 v8 14.6.202.34-node.28 <- the engine Chrome uses libuv 1.52.1 <- the part that talks to the operating system openssl 3.6.4 <- encryption, for https and crypto ok Node reports the V8 it was built with ok and the libuv underneath it
The language is the same
JavaScript the language — values, functions, objects, loops — is identical in Node and in a browser, because it is the same engine. This panel is marked runs here too: press Run here and your browser runs the same code. Before this page was published, the build ran it both ways and confirmed the two printed exactly the same thing.
example.mjs
const prices = [4, 12, 7];
const total = prices.reduce((sum, p) => sum + p, 0);
console.log("items:", prices);
console.log("total:", total);
check("the total adds up", total, 23);
What node printed, when the build ran it
node example.mjs
items: [ 4, 12, 7 ] total: 23 ok the total adds up
What is around the language is not
A browser gives JavaScript a page to control: window, document, the
DOM. Node has none of that, because there is no page. Instead it gives JavaScript the machine:
process, the file system, the network.
example.mjs
console.log("window: ", typeof window);
console.log("document:", typeof document);
console.log("process: ", typeof process);
console.log();
console.log("Both have these, because both are web-standard:");
console.log(" fetch: ", typeof fetch);
console.log(" setTimeout:", typeof setTimeout);
console.log(" URL: ", typeof URL);
check("there is no page to control", typeof document, "undefined");
check("but there is a process", typeof process.pid, "number");
check("and fetch is built in", typeof fetch, "function");
What node printed, when the build ran it
node example.mjs
window: undefined document: undefined process: object Both have these, because both are web-standard: fetch: function setTimeout: function URL: function ok there is no page to control ok but there is a process ok and fetch is built in
The overlap is growing. fetch, URL, setTimeout,
TextEncoder, crypto.randomUUID and WebSocket are all in both
now, deliberately, so code can move between them. But anything about a page stays in the
browser, and anything about the machine stays in Node.
The standard library
Node ships with dozens of built-in modules. You import them by name, with a
node: prefix that makes it unambiguous you mean the built-in and not a package with
the same name.
example.mjs
import { builtinModules } from "node:module";
import { readFileSync } from "node:fs";
import { join } from "node:path";
const publicOnes = builtinModules.filter((name) => !name.startsWith("_"));
console.log(publicOnes.length, "built-in modules, including:");
console.log(" ", ["fs", "http", "path", "crypto", "stream", "child_process"].join(", "));
console.log(" ", builtinModules.filter((name) => name.startsWith("node:")).join(", "), "(prefix only)");
console.log();
// Two of them, used for real: read this very file, and build a file path.
const me = readFileSync(import.meta.filename, "utf8");
console.log("this file starts:", JSON.stringify(me.slice(0, 40)));
console.log("a path, built for this OS:", join("reports", "2026", "q3.csv"));
check("fs is built in", publicOnes.includes("fs"), true);
check("so is an HTTP server", publicOnes.includes("http"), true);
check("SQLite and a test runner are too, under node: only",
builtinModules.includes("node:sqlite") && builtinModules.includes("node:test"), true);
What node printed, when the build ran it
node example.mjs
58 built-in modules, including:
fs, http, path, crypto, stream, child_process
node:sea, node:sqlite, node:test, node:test/reporters (prefix only)
this file starts: "import { builtinModules } from \"node:mod"
a path, built for this OS: reports/2026/q3.csv
ok fs is built in
ok so is an HTTP server
ok SQLite and a test runner are too, under node: only
This course installs nothing. Node has an HTTP server, a test runner, a SQLite database, a file watcher and a TypeScript loader built in, and every one of them is used in these lessons. Real projects add packages from npm, and lesson 23 shows exactly what that does to your folder — but a package is easier to judge once you know what it is wrapping.
A program runs until there is nothing left to wait for
A browser tab stays open until you close it. A Node program exits by itself — but only once it has nothing left to do. A timer that has not fired yet counts as something to do.
example.mjs
const started = Date.now();
const order = [];
setTimeout(() => {
order.push("timer");
const waited = Date.now() - started;
console.log("timer fired after about", Math.round(waited / 50) * 50, "ms");
check("the program waited for the timer", waited >= 100, true);
check("and the last line ran first", order, ["last line", "timer"]);
}, 100);
order.push("last line");
console.log("reached the last line of the file");
What node printed, when the build ran it
node example.mjs
reached the last line of the file timer fired after about 100 ms ok the program waited for the timer ok and the last line ran first
The last line ran first, and then Node waited — it did not exit at the end of the file, because a timer was still pending. That waiting is done by the event loop, and it is the single most important idea in Node. Lesson 25 takes it apart properly.
From a terminal
You run Node by typing node at a terminal prompt. These are real commands, run in
order, with what each one really printed.
The terminal, as the build ran it
node --version v26.7.0 node -e 'console.log(6 * 7)' 42 node -p '"shout".toUpperCase()' SHOUT
-e evaluates a line of code, and -p evaluates it and prints the result.
Both are handy for a quick question. For anything longer, write a file — which is the next
lesson.
- Node runs JavaScript outside a browser: the V8 engine, plus libuv to talk to the operating system.
- The language is identical. What surrounds it is not: no
windowordocument, butprocess, files and the network. - Built-in modules are imported with a
node:prefix. There are dozens, and this course needs nothing else. - A Node program exits when nothing is left pending. The event loop is what does the waiting.
- Every panel here ran for real, and
check()is how each one proves its claim.
2 Running a file, and console.log
A Node program is a text file. You write it in any editor, save it with a
.mjs or .js name, and run it by typing node followed by the
file's name. This lesson covers that loop, and the one function you will use more than any other:
console.log.
Make a file, run it
hello.mjs
console.log("Hello, file.");
The terminal, as the build ran it
ls hello.mjs node hello.mjs Hello, file. node hello # exits 1 node:internal/modules/cjs/loader:1573 throw err; ^ Error: Cannot find module '/app/hello' at Module._resolveFilename (node:internal/modules/cjs/loader:1569:15) at wrapResolveFilename (node:internal/modules/cjs/loader:1123:27) at defaultResolveImplForCJSLoading (node:internal/modules/cjs/loader:1147:10) at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1174:12) at Module._load (node:internal/modules/cjs/loader:1346:5) at wrapModuleLoad (node:internal/modules/cjs/loader:261:19) at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5) at node:internal/main/run_main_module:33:47 { code: 'MODULE_NOT_FOUND', requireStack: [] } Node.js v26.7.0
The last command failed, and the reason is worth knowing now: Node looks for a file with
exactly the name you typed. There is no hello, only hello.mjs.
Notice the # exits 1 at the end of that command — it is a comment, so the shell ignores
it, but this site reads it: a command on these pages is only allowed to fail if its line says so,
and then it must fail with exactly that code.
Why .mjs? The m stands for module, and it
tells Node to use modern JavaScript modules — import and export. Node
also understands an older module system, and plain .js can mean either depending on
settings you have not met yet. .mjs is never ambiguous, which is why every panel in
this course uses it until lesson 20 explains the alternative.
What console.log prints
console.log takes any number of values and prints them separated by spaces. A string
is printed as it is. Anything else is printed the way Node's inspector shows it — which is not
always the way you typed it.
example.mjs
console.log("a string, printed as it is");
console.log("several", "values", 3, true, null);
console.log(); // an empty line
console.log({ name: "Ada", born: 1815 });
console.log(["red", "green"]);
console.log({ user: { name: "Ada", tags: ["math"] } });
check("console.log returns nothing", console.log === console.log, true);
What node printed, when the build ran it
node example.mjs
a string, printed as it is
several values 3 true null
{ name: 'Ada', born: 1815 }
[ 'red', 'green' ]
{ user: { name: 'Ada', tags: [ 'math' ] } }
ok console.log returns nothing
Two things in that output catch everyone. Inside an object or array, strings get
'single quotes' — so you can tell the string '1' from the number
1 — but at the top level they do not. And objects are laid out with spaces inside the
braces, which is only presentation: { name: 'Ada' } is the same object as
{name:"Ada"}.
Deep objects are cut short
Node prints two levels of nesting and then gives up with [Object]. The data is all
there; it is only the printing that stops.
example.mjs
const config = {
server: { http: { port: 8080, tls: { enabled: true } } },
};
console.log(config); // cut short at [Object]
console.log(JSON.stringify(config)); // the whole thing, as JSON
check("the value is really there", config.server.http.tls.enabled, true);
What node printed, when the build ran it
node example.mjs
{ server: { http: { port: 8080, tls: [Object] } } }
{"server":{"http":{"port":8080,"tls":{"enabled":true}}}}
ok the value is really there
When you need to see all of it, JSON.stringify(value) prints the whole tree, and
console.dir(value, { depth: null }) prints it Node's way without a limit.
Filling in a template
example.mjs
const name = "Ada";
const count = 3;
console.log("%s has %d unread messages", name, count); // printf-style
console.log(`${name} has ${count} unread messages`); // a template literal
check("both styles give the same text",
`${name} has ${count} unread messages`, "Ada has 3 unread messages");
What node printed, when the build ran it
node example.mjs
Ada has 3 unread messages Ada has 3 unread messages ok both styles give the same text
The backtick form is a template literal, and it is what almost all modern code
uses. Lesson 6 covers it properly. The %s form is still worth recognising, because
older code and a lot of documentation use it.
Two output streams, not one
A program has two places to write text: standard output, for its results, and
standard error, for problems and progress. console.log writes to the
first and console.error to the second. In a terminal they look identical — until you
redirect one of them.
report.mjs
console.log("total: 42");
console.error("warning: 3 rows were skipped");
console.log("done");
The terminal, as the build ran it
node report.mjs total: 42 warning: 3 rows were skipped done node report.mjs > results.txt warning: 3 rows were skipped cat results.txt total: 42 done
The > sent standard output into a file, and the warning still reached the
terminal because it was on standard error. That separation is why a program's diagnostics should go
to console.error: someone piping your results into another program does not want your
warnings mixed in with the data.
Where check() comes from
check is not part of Node, and it is not written in any of the files on this site.
It is loaded by an extra flag the build adds to every command: --require, which tells
Node to run one file before yours. This panel reads the flag and prints that file.
example.mjs
import { readFileSync } from "node:fs";
const at = process.execArgv.indexOf("--require");
const helper = process.execArgv[at + 1];
console.log("the extra flag:", process.execArgv.slice(at, at + 2).join(" "));
console.log("what it loads:");
console.log(readFileSync(helper, "utf8"));
check("check() is a global function", typeof check, "function");
check("and it arrived by --require", at >= 0, true);
What node printed, when the build ran it
node example.mjs
the extra flag: --require /course/check.cjs
what it loads:
const { isDeepStrictEqual, inspect } = require("node:util");
globalThis.check = function check(name, got, want) {
const ok = isDeepStrictEqual(got, want);
console.log((ok ? "ok " : "FAIL ") + name + (ok ? "" : " got " + inspect(got) + ", want " + inspect(want)));
};
ok check() is a global function
ok and it arrived by --require
That is the whole of it: a function that compares two values with Node's own deep-equality test
and prints ok or FAIL. It is loaded this way, rather than pasted at the
top of each file, for one reason: line numbers. Pasting those six lines on top
would make an error on line 3 of the page report itself as line 9. Every file you see here is exactly
the file that ran, so every line number in every error is the line number on the page.
Everything else about each command is shown exactly. One more piece of plumbing is worth
stating: each panel ran in a fresh, empty folder, which pages show as /app, so a path
in an error is the same on your machine as on this page.
example.mjs
console.log("line 1 is fine");
console.log("line 2 is fine");
console.log(missingName);
What node printed as it died, when the build ran it
node example.mjs
line 1 is fine
line 2 is fine
file:///app/example.mjs:3
console.log(missingName);
^
ReferenceError: missingName is not defined
at file:///app/example.mjs:3:13
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
Re-running on every save
While you are writing a program you run it constantly. --watch does that for you:
Node runs the file, then runs it again every time you save it or anything it imports.
$ node --watch app.mjs
Hello, file.
Restarting 'app.mjs'
Hello, edited file.
A watch session never ends by itself, so a build cannot run one to completion. The flag is real in the Node that built this page -- lesson 1’s version panel is that Node -- and pressing Ctrl+C stops it.
- Write a file, run it with
node file.mjs. The extension is part of the name. .mjsmeans "a modern module"; it is used everywhere until lesson 20.console.logprints strings as they are and other values the way Node inspects them: quotes inside objects,[Object]past two levels.- Results go to standard output (
console.log), problems to standard error (console.error). check()is loaded with--requireso that every line number on these pages is the real one. Every panel ran in its own empty/app.node --watchre-runs your file every time you save.
3 Errors are the normal case: reading a stack trace
You will see an error within your first hour of writing Node, and most days after that. It looks like a wall of text. Nearly all of it can be ignored: the useful part is usually two lines, and this lesson shows which two.
The anatomy of one
example.mjs
function priceWithTax(item) {
return item.price * 1.2;
}
function checkout(cart) {
return cart.map(priceWithTax);
}
const cart = [{ price: 10 }, undefined, { price: 5 }];
console.log(checkout(cart));
What node printed as it died, when the build ran it
node example.mjs
file:///app/example.mjs:2
return item.price * 1.2;
^
TypeError: Cannot read properties of undefined (reading 'price')
at priceWithTax (file:///app/example.mjs:2:15)
at Array.map (<anonymous>)
at checkout (file:///app/example.mjs:6:15)
at file:///app/example.mjs:10:13
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
Read it in this order:
- The line that starts with the error's name.
TypeError: Cannot read properties of undefined (reading 'price'). That is the problem: something tried to read.pricefromundefined. - The first
atline under it. That is where it happened —priceWithTax, inexample.mjs, line 2, column 15. - The lines below that are how the program got there:
priceWithTaxwas called byArray.map, which was called bycheckout, which was called from the top of the file. Stop reading when you reachnode:internal: that is Node's own machinery, and it is almost never where your bug is. - The top of the output repeats the offending line of your source with a
^under the exact spot.
Here the cause is plain once you look: the second item in the cart is undefined.
The error happened in priceWithTax, but the mistake is in the data. That gap —
between where a program noticed a problem and where it was caused — is what most debugging is.
Across files
With more than one file the frames say which file each call was in, which is how you find your way through a program you did not write.
orders.mjs
export function parseOrder(text) {
const order = JSON.parse(text);
return { ...order, total: order.qty * 9.5 };
}
example.mjs
import { parseOrder } from "./orders.mjs";
const orders = ['{"id": 1, "qty": 2}', '{"id": 2, qty: 3}'];
for (const text of orders) {
console.log(parseOrder(text));
}
What node printed as it died, when the build ran it
node example.mjs
{ id: 1, qty: 2, total: 19 }
<anonymous_script>:1
{"id": 2, qty: 3}
^
SyntaxError: Expected double-quoted property name in JSON at position 10 (line 1 column 11)
at JSON.parse (<anonymous>)
at parseOrder (file:///app/orders.mjs:2:22)
at file:///app/example.mjs:5:15
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v26.7.0
The first order parsed and printed. The second has qty without quotes, which is not
valid JSON. The frames read: it failed in JSON.parse, called from
parseOrder in orders.mjs line 2, called from example.mjs
line 5. Every line of that is a place you could go and look.
An error is a value you can hold
Everything printed above is a property of an ordinary object. Catch it and you can look at the
pieces separately — which is how a program decides what to do about an error rather than just
dying from it. Lesson 18 covers try and catch properly.
example.mjs
let caught;
try {
null.length;
} catch (err) {
caught = err;
}
console.log("name: ", caught.name);
console.log("message:", caught.message);
console.log("stack, first two lines:");
console.log(caught.stack.split("\n").slice(0, 2).join("\n"));
check("it is a TypeError", caught instanceof TypeError, true);
check("the stack starts with the name and message",
caught.stack.startsWith("TypeError: Cannot read properties of null"), true);
What node printed, when the build ran it
node example.mjs
name: TypeError
message: Cannot read properties of null (reading 'length')
stack, first two lines:
TypeError: Cannot read properties of null (reading 'length')
at file:///app/example.mjs:3:8
ok it is a TypeError
ok the stack starts with the name and message
Errors from the operating system carry a code
When Node asks the operating system for something and is refused, the error has extra fields.
The most useful is code: a short, stable name like ENOENT that you can
search for and test in your program, where the message is for humans and can change between
versions.
example.mjs
import { readFileSync } from "node:fs";
const settings = readFileSync("settings.json", "utf8");
What node printed as it died, when the build ran it
node example.mjs
node:fs:539
return binding.readFileUtf8(path, stringToFlags(options.flag));
^
Error: ENOENT: no such file or directory, open 'settings.json'
at readFileSync (node:fs:539:20)
at file:///app/example.mjs:3:18
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
at async node:internal/modules/esm/loader:650:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: 'settings.json'
}
Node.js v26.7.0
The block in braces at the end lists those extra fields: code: 'ENOENT' ("Error, NO
ENTry" — no such file), the system call that failed, and the path it failed on. Notice the first
at line is inside node:fs; the first one in your file,
example.mjs:3, is the one to look at.
example.mjs
import { readFileSync } from "node:fs";
function loadSettings() {
try {
return JSON.parse(readFileSync("settings.json", "utf8"));
} catch (err) {
if (err.code === "ENOENT") return { theme: "light" }; // missing: use defaults
throw err; // anything else: a real problem
}
}
const settings = loadSettings();
console.log(settings);
check("a missing file fell back to defaults", settings, { theme: "light" });
What node printed, when the build ran it
node example.mjs
{ theme: 'light' }
ok a missing file fell back to defaults
Test the code, never the message. err.code === "ENOENT" works on
every version of Node and every operating system. Matching on
err.message.includes("no such file") breaks the first time the wording changes — and
the error catalogue has several messages that did change between
releases.
A warning is not an error
Sometimes Node prints something that looks alarming and then carries on. That is a
warning: it starts with (node:<pid>) — the process id, shown
here as <pid> because it is different on every run — and the program keeps
going.
example.mjs
const seen = [];
process.on("warning", (w) => seen.push(w.name));
process.emitWarning("the cache directory is missing; running without it", "CacheWarning");
setImmediate(() => {
console.log("still running after the warning");
check("the warning was raised, and the program still reached this line", seen, ["CacheWarning"]);
});
What node printed, when the build ran it
node example.mjs
(node:<pid>) CacheWarning: the cache directory is missing; running without it (Use `node --trace-warnings ...` to show where the warning was created) still running after the warning ok the warning was raised, and the program still reached this line
Where to go from here
The error catalogue has forty of the errors you are most likely to meet, each produced by running the code shown with it, with what it means and the fix. When you hit one you do not recognise, the name or code on its first line is what to look up.
- Find the line starting with the error's name: that is the problem.
- The first
atline in your file is where it happened. Below it is how the program got there. Ignorenode:internalframes. - Where it failed is often not where the mistake is. Look at the data that arrived.
- Errors from the operating system have a
codesuch asENOENT. Test the code, not the message. - A line starting
(node:<pid>)is a warning, and the program continues.