Debugging
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Node.js 26 (26.10.0, released 2026-09-21, the Current line that becomes LTS on 2026-10-28), with the differences
of Node.js 24 "Krypton" (24.21.0, Active LTS) and Node.js 22 "Jod" (22.23.3, Maintenance) called out; versions and stability levels were
checked against the official documentation on 2026-10-04, and every example below was run on all three lines unless the text says otherwise.
Debugging in Node.js is built on one mechanism: the V8 inspector, a small server inside the process that speaks the Chrome DevTools Protocol
(CDP) over a WebSocket. Chrome DevTools, VS Code, JetBrains IDEs and the command-line node inspect client are all just different front ends for
that one protocol, so once you understand how to switch the inspector on, where it listens and who may talk to it, every tool works the same way.
The node:inspector module and the node inspect command-line debugger are Stability 2 (Stable); the new non-interactive probe mode of
node inspect is Stability 1 (Experimental); network inspection is Stability 1 (Experimental) and the node:inspector helpers that feed it are
Stability 1.1 (Active development); --enable-source-maps is stable while the helper functions in node:module are still Stability 1
(Experimental). The --trace-* flags and NODE_DEBUG are long-standing command-line and environment features that carry no stability index.
The running scenario is the Bookshelf application that grows across this section, described in
Introduction & Architecture; this page is about stopping a program and looking inside it.
Measuring where the time goes is the subject of Profiling & Performance, finding leaked memory is in
Memory & Leaks, and post-mortem evidence from production is in
Diagnostic Reports, Logging & Observability.
The stability and version map
| API or flag | Stability (Node.js 26 docs) | Version notes |
|---|---|---|
|
No stability index |
|
|
No stability index |
Added in v22.2.0 and v20.15.0, so it works on all three lines covered here. |
|
2 Stable |
|
|
2 Stable |
Replaces the legacy |
|
1 Experimental |
Added in v26.1.0 and v24.16.0; not present on Node.js 22. |
|
No stability index (no longer experimental) |
Added in v23.7.0 and v22.14.0; no longer experimental since v24.8.0 and v22.20.0. |
|
No stability index (no longer experimental) |
Added in v12.12.0; no longer experimental since v15.11.0 and v14.18.0. |
|
1 Experimental (the whole source-map section of |
|
|
No stability index |
|
|
No stability index |
Default is 10 frames; |
|
|
|
|
1 Experimental (flag); 1.1 Active development (helpers) |
Flag added in v22.6.0 and v20.18.0; WebSocket events in the helpers added in v24.7.0. |
|
1.1 Active development |
Added in v24.1.0 and v22.17.0. |
|
1.0 Early development |
Added in v25.0.0 and v24.12.0; there is no such flag on Node.js 22 (observed below). |
How the inspector works
When you start Node.js with --inspect, the process opens a TCP server (by default on 127.0.0.1:9229) and registers itself as a debug
target. A client first makes a plain HTTP request to learn the target’s address, then upgrades to a WebSocket whose URL ends with a random
UUID, and from then on exchanges JSON messages: commands such as Debugger.setBreakpointByUrl or Runtime.evaluate, and events such as
Debugger.paused. Everything a debugger can do, from stepping to evaluating an expression in a paused frame, is one of those messages, which
is why the protocol is also the foundation for the profilers described in
Profiling & Performance. The UUID is not a password in the cryptographic sense, but a
client must know host, port and UUID to connect, and by default Node.js prints the full URL on stderr and lists it at the /json/list endpoint
of the same port.
node --inspect=127.0.0.1:0 hang.js &
curl -s http://127.0.0.1:<port>/json/list
Debugger listening on ws://127.0.0.1:50175/c5025569-b627-4be5-8f65-8a7f42ecb830
For help, see: https://nodejs.org/learn/getting-started/debugging
[ {
"description": "node.js instance",
"devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:50175/c5025569-...",
"devtoolsFrontendUrlCompat": "devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:50175/c5025569-...",
"faviconUrl": "https://nodejs.org/static/images/favicons/favicon.ico",
"id": "c5025569-b627-4be5-8f65-8a7f42ecb830",
"title": "hang.js",
"type": "node",
"url": "file:///project/hang.js",
"webSocketDebuggerUrl": "ws://127.0.0.1:50175/c5025569-b627-4be5-8f65-8a7f42ecb830"
} ]
Node.js 24 and 22 print the same two lines and the same JSON shape. Port 0 asks the operating system for a free port, which is what makes the
example safe to run next to other services and is the habit to adopt for anything automated; use the fixed default 9229 only when a human is
going to type the address into a tool.
Turning the inspector on
The Learn guide and the CLI reference describe the same family of flags. They differ in what happens to your program while it waits for a client, and that difference decides which one to use.
| Flag | What happens to your code | Use it when |
|---|---|---|
|
Listens and keeps running. The program starts immediately; a debugger that attaches later pauses it only at breakpoints. |
You debug a long-running server and attach when something goes wrong. |
|
Listens and waits for a debugger to attach before running any user code, but does not stop on the first line. |
You need a client attached from the start but want your own breakpoints, not an automatic pause (typical for scripts that finish quickly). |
|
Listens, waits for a debugger, then breaks on the first line of user code. |
You want to step from the very beginning, for example to debug start-up code or module loading. |
The next program prints one line. Run with each flag and a two-second alarm, it shows the difference without any debugger attached.
// hello.js
console.log('user code is running');
$ node --inspect=127.0.0.1:0 hello.js
Debugger listening on ws://127.0.0.1:<port>/<uuid>
user code is running
$ node --inspect-wait=127.0.0.1:0 hello.js
Debugger listening on ws://127.0.0.1:<port>/<uuid>
(nothing else: the process is waiting for a debugger)
$ node --inspect-brk=127.0.0.1:0 hello.js
Debugger listening on ws://127.0.0.1:<port>/<uuid>
(nothing else: the process is waiting, and would pause on the first line once attached)
The flags take an optional [host:]port; other flags and variables that touch the inspector are summarised below.
| Option | Meaning |
|---|---|
|
Sets the address to use when the inspector is activated later, for example by |
|
Chooses where the WebSocket URL is published: on stderr, at |
|
Removes the ability to start the inspector by sending |
|
The port used by the debugger when it is enabled. The API page shows it being assigned, and |
|
Can carry |
|
Read by |
Source: Node.js API: --inspect-port, Node.js API: --inspect-publish-uid, Node.js API: process.debugPort
Switching the inspector on in a running process
You do not have to decide at start-up. On POSIX systems a Node.js process starts its inspector when it receives SIGUSR1; the signal does not
exist on Windows. The address comes from --inspect-port (or the default 127.0.0.1:9229). A program can also open the inspector itself with
inspector.open(), which is equivalent to --inspect=[[host:]port], and inspector.url() reports the active URL or undefined.
node --inspect-port=127.0.0.1:0 hang.js &
kill -USR1 $!
Debugger listening on ws://127.0.0.1:50175/ef33135d-6051-4046-a66d-97cc4fd293c5
For help, see: https://nodejs.org/learn/getting-started/debugging
Started with --disable-sigusr1, the same process stayed alive and printed nothing when it received SIGUSR1, on all three lines. The flag
is therefore the way to make sure nobody can switch the inspector on from outside the process, which matters in the
signal handling of a production service.
// open.js
import inspector, { Session } from 'node:inspector/promises';
console.log('before:', inspector.url()); // undefined: not listening yet
inspector.open(0, '127.0.0.1'); // port 0 = pick a free port
console.log('after:', inspector.url()?.replace(/:\d+\//, ':<port>/').replace(/[0-9a-f-]{36}$/, '<uuid>'));
const session = new Session();
session.connect(); // same-thread session
const { result } = await session.post('Runtime.evaluate', {
expression: 'process.version',
});
console.log('evaluated in-process:', result.value);
session.disconnect();
inspector.close();
console.log('closed:', inspector.url());
before: undefined
Debugger listening on ws://127.0.0.1:50842/d4ac12c7-b063-4f7e-8bb3-c0b406bbcb62
For help, see: https://nodejs.org/learn/getting-started/debugging
after: ws://127.0.0.1:<port>/<uuid>
evaluated in-process: v26.10.0
closed: undefined
An in-process Session is for evaluating expressions and collecting profiles, as in the CPU and heap profiler examples of the
node:inspector page. The API documentation warns against setting breakpoints through a same-thread session, because the paused program is
the debugger itself; connect to the main thread from a worker with session.connectToMainThread(), or attach from outside over the WebSocket.
inspector.waitForDebugger() blocks until a client has sent Runtime.runIfWaitingForDebugger, which is how you add the behaviour of
--inspect-wait to a program that decides at run time whether it is being debugged.
Chrome DevTools
Chrome DevTools is the most capable front end, and the one to reach for first. There are two ways to attach, both from the Learn guide.
| Method | Steps |
|---|---|
Built-in UI |
Open |
Manual |
Visit |
Once attached you get the Sources panel that you know from the browser, documented in the Chrome DevTools JavaScript debugging reference (call stack, scope, watch expressions, the console and an ignore list for skipping library code while stepping). The breakpoint types that make sense in Node.js are the line-of-code breakpoint, the conditional line-of-code breakpoint, the logpoint (which logs to the console without pausing), the exception breakpoint (pause on the line that throws a caught or uncaught exception) and the function breakpoint; the DOM, XHR and event-listener breakpoints of the browser documentation have no Node.js equivalent. The same window also hosts the profilers used in Profiling & Performance and Memory & Leaks.
What DevTools actually sends
It is worth seeing the protocol once, because it demystifies every front end and gives you a way to script a debugger. The next program is a
complete, dependency-free CDP client: it starts a target with --inspect-brk, reads the WebSocket URL from stderr, connects with the global
WebSocket class, waits for the automatic pause on the first line, resumes, waits for the debugger; statement inside price() and reads a
local variable from the paused frame.
// app.js - the target we want to debug
function price(book, discount) {
const total = book.price * (1 - discount);
debugger; // pauses only when a debugger is attached
return Math.round(total * 100) / 100;
}
console.log(price({ title: 'Dune', price: 12.5 }, 0.2));
// cdp.mjs
import { spawn } from 'node:child_process';
const child = spawn(process.execPath, ['--inspect-brk=127.0.0.1:0', 'app.js'], {
stdio: ['ignore', 'inherit', 'pipe'],
});
// Read the WebSocket URL that the inspector prints on stderr.
const url = await new Promise((resolve) => {
child.stderr.setEncoding('utf8');
child.stderr.on('data', (text) => {
const match = /ws:\/\/\S+/.exec(text);
if (match) resolve(match[0]);
});
});
// Speak JSON-RPC over the global WebSocket client.
const ws = new WebSocket(url);
let nextId = 0;
const pending = new Map();
const pauses = [];
ws.onmessage = ({ data }) => {
const message = JSON.parse(data);
if (message.id) pending.get(message.id)?.(message.result);
else if (message.method === 'Debugger.paused') pauses.shift()?.(message.params);
};
const send = (method, params = {}) =>
new Promise((resolve) => {
const id = ++nextId;
pending.set(id, resolve);
ws.send(JSON.stringify({ id, method, params }));
});
const nextPause = () => new Promise((resolve) => pauses.push(resolve));
await new Promise((resolve) => (ws.onopen = resolve));
await send('Debugger.enable');
const first = nextPause();
await send('Runtime.runIfWaitingForDebugger');
// --inspect-brk pauses before the first line of user code.
let paused = await first;
console.log('paused at start:', paused.reason);
// Resume; the debugger statement pauses us again inside price().
const second = nextPause();
await send('Debugger.resume');
paused = await second;
const frame = paused.callFrames[0];
console.log('paused in:', frame.functionName, 'line', frame.location.lineNumber + 1);
const { result } = await send('Debugger.evaluateOnCallFrame', {
callFrameId: frame.callFrameId,
expression: 'total',
});
console.log('total =', result.value);
await send('Debugger.resume');
ws.close();
paused at start: Break on start
paused in: price line 4
total = 10
10
Line numbers in the protocol are zero-based, which is why the client adds one. The final 10 is the program’s own output, printed after the
last resume; running node app.js without any debugger prints just that line, because the debugger; statement is ignored when no
debugger is attached.
Conditional breakpoints and logpoints are just expressions
The conditional breakpoint and the logpoint of the Sources panel are not special features of Node.js: the front end sets an ordinary
breakpoint with a condition expression. The condition is evaluated in the paused frame; if it is falsy, execution continues. A logpoint is
the same thing with a condition that logs and then evaluates to false, so the program never actually stops. The client below sets both on
a small loop and prints what the conditional breakpoint saw.
// loop.js
const books = [12.5, 8, 30, 4.25];
let total = 0;
for (const price of books) {
total += price;
}
console.log('total', total);
// breakpoints.mjs
import { spawn } from 'node:child_process';
const child = spawn(process.execPath, ['--inspect-brk=127.0.0.1:0', 'loop.js'], {
stdio: ['ignore', 'inherit', 'pipe'],
});
const url = await new Promise((resolve) => {
child.stderr.setEncoding('utf8');
child.stderr.on('data', (t) => {
const m = /ws:\/\/\S+/.exec(t);
if (m) resolve(m[0]);
});
});
const ws = new WebSocket(url);
let id = 0;
const waiting = new Map();
const send = (method, params = {}) =>
new Promise((resolve) => {
waiting.set(++id, resolve);
ws.send(JSON.stringify({ id, method, params }));
});
const evaluate = async (frame, expression) =>
(await send('Debugger.evaluateOnCallFrame', { callFrameId: frame.callFrameId, expression })).result.value;
ws.onmessage = async ({ data }) => {
const m = JSON.parse(data);
if (m.id) return waiting.get(m.id)?.(m.result);
if (m.method !== 'Debugger.paused') return;
const [top] = m.params.callFrames;
if (m.params.reason === 'Break on start') return send('Debugger.resume');
console.log(`conditional breakpoint hit: price=${await evaluate(top, 'price')} total=${await evaluate(top, 'total')}`);
send('Debugger.resume');
};
await new Promise((resolve) => (ws.onopen = resolve));
await send('Debugger.enable');
// Line numbers are 0-based in the protocol: line 5 of loop.js is lineNumber 4.
await send('Debugger.setBreakpointByUrl', {
urlRegex: 'loop\\.js$',
lineNumber: 4,
condition: 'price > 10', // a conditional breakpoint
});
await send('Debugger.setBreakpointByUrl', {
urlRegex: 'loop\\.js$',
lineNumber: 6,
condition: "console.log('logpoint: total is', total), false", // a logpoint never pauses
});
await send('Runtime.runIfWaitingForDebugger');
child.on('exit', () => ws.close());
conditional breakpoint hit: price=12.5 total=0
conditional breakpoint hit: price=30 total=20.5
logpoint: total is 54.75
total 54.75
The practical consequence is that a condition or logpoint can contain any JavaScript, including side effects, and that it runs inside your process on every hit, so a heavy expression on a hot line slows the program down.
The VS Code debugger
VS Code has built-in debugging for Node.js, and it is the front end most day-to-day work uses. It speaks the same protocol, so everything above
applies; what it adds is project integration. The editor offers three ways in, listed on the official page: auto attach (the debugger attaches
automatically to Node.js processes started from the integrated terminal, with smart, always and onlyWithFlag modes), the JavaScript
debug terminal (created with the "Debug: Create JavaScript Debug Terminal" command, where every Node.js process you run is debugged), and a
launch.json file in .vscode/ that either launches your program or attaches to one that is already running.
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Bookshelf",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/src/server.js",
"envFile": "${workspaceFolder}/.env",
"skipFiles": ["<node_internals>/**"]
},
{
"name": "Attach to local process",
"type": "node",
"request": "attach",
"port": 9229,
"restart": true
},
{
"name": "Attach to container",
"type": "node",
"request": "attach",
"address": "127.0.0.1",
"port": 9229,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/app"
}
]
}
This file was checked to be valid JSON, and the attribute names were checked against the VS Code documentation, but the debugging sessions themselves were not driven from VS Code for this page (the protocol exchanges they rely on are the ones run above).
| Attribute | Meaning |
|---|---|
|
The entry point to launch. |
|
Glob patterns of files the debugger steps over, so stepping does not wander into Node.js internals; |
|
Loads variables from a file instead of putting them in |
|
Where an |
|
Reconnects automatically when the target restarts, which suits |
|
Map paths between your workspace and a remote file system (a container, another machine) so that breakpoints bind to the editable local files. |
|
Break when the program launches; or continue if the process is paused on attach. |
|
Where the generated JavaScript and its source maps live, for transpiled projects (see Source maps). |
|
Attach to an already running process by sending |
Other tools are listed on the Learn page, which names Visual Studio 2017 and later, JetBrains WebStorm and other JetBrains IDEs (a Node.js
debug configuration uses the inspector by default), the Eclipse IDE with the Wild Web Developer extension, and the chrome-remote-interface
library for scripting. They all connect to the same endpoint, so a flag that works for one works for all.
The command-line debugger: node inspect
node inspect is a small CLI client for the same inspector. It is not a full debugger, but it needs no browser or editor, which makes it the
tool for a bare server, a container shell or an SSH session. It starts your script in a child process under --inspect, attaches to it and
gives you a debug> prompt. Besides node inspect script.js it accepts host:port to attach to a process that is already listening and
-p <pid> to attach by process id (the client sends SIGUSR1, so on POSIX it can attach to a process that was not started in debug mode).
$ node inspect app.js
< Debugger listening on ws://127.0.0.1:9229/<uuid>
< For help, see: https://nodejs.org/learn/getting-started/debugging
connecting to 127.0.0.1:9229 ... ok
< Debugger attached.
Break on start in app.js:1
> 1 // app.js - the target we want to debug
2 function price(book, discount) {
3 const total = book.price * (1 - discount);
debug> sb(4)
debug> cont
break in app.js:4
2 function price(book, discount) {
3 const total = book.price * (1 - discount);
> 4 debugger; // pauses only when a debugger is attached
5 return Math.round(total * 100) / 100;
6 }
debug> exec total
10
debug> exec book.title
'Dune'
debug> bt
#0 price app.js:4:2
#1 (anonymous) app.js:8:12
#2 run node:internal/modules/esm/module_job:569:24
debug> watch("discount")
debug> next
step in app.js:5
Watchers:
0: discount = 0.2
3 const total = book.price * (1 - discount);
* 4 debugger; // pauses only when a debugger is attached
> 5 return Math.round(total * 100) / 100;
6 }
debug> .exit
The session was run on Node.js 26 with the commands piped in; on Node.js 24 and 22 the same commands stopped at the same line and exec total
printed 10. The port in the first line is the default 9229: node inspect uses it unless you pass --port, so two sessions on one
machine need different ports. Attaching by pid worked too: node inspect -p <pid> printed "Debugger
listening", connected, evaluated process.pid in the target and printed "Debugger ending" on exit.
| Command | Effect |
|---|---|
|
Continue, step over, step into, step out, pause running code. |
|
Set a breakpoint on the current line, a line, a function, or a line of another file with an optional condition. A breakpoint in a module that is not loaded yet works and prints a warning. |
|
Evaluate one expression in the paused frame, or open a REPL in the debugged script’s context (Ctrl+C leaves it). |
|
Evaluate expressions at every stop and print them before the source listing. |
|
Print the call stack, list source with context, list loaded scripts, show the V8 version. |
|
Run, restart or kill the debugged script. |
|
Record CPU profiles and write heap snapshots from the prompt; see Profiling & Performance and Memory & Leaks. |
Pressing Enter repeats the previous command. NODE_INSPECT_RESUME_ON_START=1 makes the session run until the first debugger; statement or
breakpoint instead of breaking on the first line.
Probe mode: printf debugging without editing the code
Probe mode turns a debugging session into a one-line command. You name a source location and an expression; Node.js starts your script under
the inspector, evaluates the expression every time execution reaches that location, and prints one report when the program ends. It is
Stability 1 (Experimental), it only launches a new process from an entry-point script (it cannot attach), and it exists on Node.js 26 and
Node.js 24 (24.16.0 and later) but not on Node.js 22, where the flag is rejected. It is also designed for tools and coding agents, because
--json produces a structured report.
node inspect --probe loop.js:5 --expr 'total' --cond 'price > 10' loop.js
Hit 1 at file:///project/loop.js:5:3
total = 0
Hit 2 at file:///project/loop.js:5:3
total = 20.5
Completed
The same command with --json printed (on Node.js 26) a single line starting {"v":2,"probes":[...],"results":[...]}, with one hit event per
evaluation and a final terminal event such as completed. The --cond, --max-hit and --json runs were executed; the remaining rows come from
the API page:
| Option or rule | Behaviour |
|---|---|
|
A strict pair. The file is matched as a path suffix against every loaded script, so |
|
Optional, written after its pair; the probe records a hit only when the expression is truthy at that location. A condition that throws counts as false. |
|
Optional and per probe. In a run with |
|
Global options: structured output (schema version 2), property previews for objects, a wall-clock deadline (default 30000 ms), and the inspector port (default |
Probing a |
Produces a |
Missing a probe and exit status |
A probe that is never reached is reported as missed; |
When the child needs its own Node.js flags, separate them with --, as in node inspect --probe app.js:10 --expr user — --no-warnings app.js.
The debugger statement
debugger; is a standard JavaScript statement, not a Node.js feature: it is a breakpoint written into the source. With a debugger attached
(DevTools, VS Code or node inspect) execution pauses there; with none attached it does nothing, which is why the app.js target above
printed 10 when run plainly and paused when run under the inspector. That is convenient for a bug that appears only after a long run: put
the statement where you want to look, start the process with --inspect, attach when the process reaches it. The cost is that you must not
forget to delete it; a linter rule such as no-debugger (see ESLint & Prettier) catches it before commit.
For a program that starts without any flag but should wait on a debugger; statement only when asked, combine it with inspector.open()
and inspector.waitForDebugger() (shown earlier) behind an environment variable of your own.
Debugging a test
Debug a single test file the way you debug any script: a file that uses node:test is an ordinary program, so
node --inspect-brk test/app.test.js waits for a debugger and pauses before the first line; this was checked on Node.js 26. The
test runner is the subtle case: the CLI reference says --test cannot be combined with the inspector,
but on Node.js 26, 24 and 22 the combination started and printed a warning instead (observed): "Using the inspector with --test forces
running at a concurrency of 1", with a pointer to the inspectPort option. That option of run() sets the inspector port of each test child
process (a nullish value gives each its own port, counting up from the primary’s process.debugPort), and it is ignored when
isolation is 'none'. Treat the whole-suite combination as supported-by-observation only, and prefer debugging one file.
Source maps
Debuggers and stack traces show positions in the JavaScript that actually runs. When that JavaScript is generated (bundled, minified or
transpiled from TypeScript), the positions mean nothing to you. A source map is a JSON file, referenced by a //# sourceMappingURL=
comment at the end of the generated file, that maps positions in the output back to the original sources. Node.js supports the ECMA-426
source map format, and there are two separate consumers of it: the debugger front ends (DevTools and VS Code read the map themselves, so
breakpoints and stepping work in your .ts files with no Node.js flag), and the runtime, which needs --enable-source-maps before it
rewrites the stack traces of errors. The example bundles two tiny TypeScript files with esbuild (installed in a scratch project; the same
idea applies to tsc --sourceMap) and throws from the first one.
export function checkDigit(isbn: string): number {
const digits = isbn.replaceAll('-', '');
if (digits.length !== 13) {
throw new RangeError(`ISBN must have 13 digits, got ${digits.length}`);
}
return Number(digits[12]);
}
import { checkDigit } from './isbn.ts';
interface Book {
title: string;
isbn: string;
}
const book: Book = { title: 'Dune', isbn: '978-0-441-17271' };
console.log(checkDigit(book.isbn));
npx esbuild src/main.ts --bundle --platform=node --format=esm --sourcemap --outfile=dist/main.js
node dist/main.js
node --enable-source-maps dist/main.js
$ node dist/main.js
file:///project/dist/main.js:5
throw new RangeError(`ISBN must have 13 digits, got ${digits.length}`);
^
RangeError: ISBN must have 13 digits, got 12
at checkDigit (file:///project/dist/main.js:5:11)
at file:///project/dist/main.js:12:13
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
$ node --enable-source-maps dist/main.js
/project/src/isbn.ts:4
throw new RangeError(`ISBN must have 13 digits, got ${digits.length}`);
^
RangeError: ISBN must have 13 digits, got 12
at checkDigit (/project/src/isbn.ts:4:11)
at <anonymous> (/project/src/main.ts:9:13)
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
The second trace points at isbn.ts line 4 and main.ts line 9, the lines you wrote, instead of positions in the bundle. The flag has
costs and limits that the CLI reference spells out: it caches source maps and makes a best effort; reading Error.stack becomes slower, so
a program that builds many stack traces should measure before enabling it in production; and overriding Error.prepareStackTrace can stop
the mapping unless your override calls and returns the original function. Setting the flag in NODE_OPTIONS is allowed, which is the usual
way to enable it for every process of a service.
// boot.js
import { setSourceMapsSupport, getSourceMapsSupport } from 'node:module';
setSourceMapsSupport(true, { nodeModules: false, generatedCode: false });
console.log(getSourceMapsSupport());
await import('./dist/main.js'); // loaded after the call, so its map is parsed
[Object: null prototype] {
enabled: true,
nodeModules: false,
generatedCode: false
}
setSourceMapsSupport() has the same effect as the flag, plus two options: nodeModules (also map files inside node_modules, default
false) and generatedCode (map eval and new Function code, default false). Only modules loaded after the call have their maps parsed,
so the documentation recommends the command-line flag. module.findSourceMap(path) returns the SourceMap for a file, and NODE_V8_COVERAGE
also turns on source map parsing. These functions are Stability 1 (Experimental) even though the flag is not, so keep calls to them behind a
small wrapper.
TypeScript run directly needs no source map
Since Node.js strips types by replacing them with whitespace, the line and column numbers of the running code already equal those of the
.ts file. The Node.js TypeScript documentation says so explicitly, and it was visible when node src/main.ts was run on Node.js 26, 24
and 22: the trace pointed at src/isbn.ts:4 without --enable-source-maps and without producing any map. The flag matters again only when
you compile (esbuild, tsc, SWC) or bundle. See TypeScript in Node.js and the compiler options in
TSConfig & Compiler Options.
A debugger front end that cannot fetch the map itself (for example because the map is served by a build step) can be given the resource:
inspector.NetworkResources.put(url, content) answers the front end’s Network.loadNetworkResource request, and needs the flag
--experimental-inspector-network-resource (Stability 1.1, added in v24.5.0 and v22.19.0). It is documented but was not run for this page.
The --trace-* flags
The --trace-* flags change what Node.js prints when something happens that normally leaves no trail: a warning with no origin, a hidden
exit, a synchronous call on a hot path. They are for investigation, not for production defaults, and each is a one-line change you can try
without touching the code.
--trace-warnings and --trace-deprecation
By default a process warning is one line and says nothing about where it was raised. --trace-warnings adds the stack trace for every
warning (deprecations included) and --trace-deprecation only for deprecations. The program below triggers a real deprecation,
DEP0005 (the Buffer() constructor, an application-level runtime deprecation since v10.0.0), and a custom warning.
// warn.js
import { Buffer } from 'node:buffer';
function legacyCode() {
return new Buffer(8); // DEP0005: runtime deprecation
}
legacyCode();
process.emitWarning('catalogue is stale', { code: 'BOOKSHELF_STALE' });
$ node warn.js
(node:74998) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), ...
(Use `node --trace-deprecation ...` to show where the warning was created)
(node:74998) [BOOKSHELF_STALE] Warning: catalogue is stale
$ node --trace-warnings warn.js
(node:74999) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. ...
at showFlaggedDeprecation (node:buffer:238:11)
at new Buffer (node:buffer:318:3)
at legacyCode (file:///project/warn.js:5:10)
at file:///project/warn.js:7:1
(node:74999) [BOOKSHELF_STALE] Warning: catalogue is stale
at file:///project/warn.js:8:9
With --trace-deprecation the deprecation got its stack but the custom warning was still one line, followed by the hint to use --trace-warnings.
Three related flags control what happens instead of just printing: --throw-deprecation turns a deprecation into a thrown error (run on
Node.js 26 it raised DeprecationWarning from showFlaggedDeprecation and ended the process, which makes it a good CI setting to fail on use
of deprecated APIs), --no-deprecation silences deprecations, and --pending-deprecation also reports deprecations that are still
documentation-only. The --disable-warning=<code-or-type> flag silences one warning by code or type and has been Stability 2 (Stable) since
v26.7.0. In your own code, process.on('warning', …) receives every warning object, which is the place to route them to a logger.
--trace-uncaught
An uncaught Error already carries the stack from where it was created. When you throw something that is not an Error, or an error
that was created elsewhere, you lose where it was thrown. --trace-uncaught prints that second stack under a "Thrown at:" heading. The
CLI reference warns that enabling it may affect garbage collection negatively, so it is an investigation flag.
// uncaught.js
function save(isbn) {
if (isbn.length !== 13) throw { code: 'E_ISBN', isbn };
}
setTimeout(() => save('123'), 10);
$ node uncaught.js
file:///project/uncaught.js:3
if (isbn.length !== 13) throw { code: 'E_ISBN', isbn };
^
{ code: 'E_ISBN', isbn: '123' }
Node.js v26.10.0
$ node --trace-uncaught uncaught.js
file:///project/uncaught.js:3
if (isbn.length !== 13) throw { code: 'E_ISBN', isbn };
^
{ code: 'E_ISBN', isbn: '123' }
Thrown at:
at save (file:///project/uncaught.js:3:27)
at file:///project/uncaught.js:6:18
at listOnTimeout (node:internal/timers:685:17)
at processTimers (node:internal/timers:618:7)
Node.js v26.10.0
Source: Node.js API: --trace-uncaught
The fix for the code is to throw Error objects (or subclasses with a code), as described in
Errors & Error Handling and in
JavaScript exception handling; the flag is how you find the offender in
code you do not control.
--trace-sync-io
A Node.js server stays fast only while the event loop is free (see Don’t Block the Event Loop).
--trace-sync-io prints a stack trace whenever synchronous I/O is detected after the first turn of the event loop, which is exactly
the allowance for start-up code: reading a config file synchronously at load is fine, doing it in a request handler is not.
// sync.js
import { readFileSync } from 'node:fs';
const config = readFileSync(new URL(import.meta.url)); // before the first turn: ignored
setTimeout(() => {
const again = readFileSync(new URL(import.meta.url)); // later: reported
console.log(config.length === again.length);
}, 10);
(node:94911) WARNING: Detected use of sync API
at openSync (node:fs:778:18)
at readFileSync (node:fs:624:35)
at file:///project/sync.js:7:17
at listOnTimeout (node:internal/timers:685:17)
at processTimers (node:internal/timers:618:7)
(node:94911) WARNING: Detected use of sync API
at tryStatSync (node:fs:520:25)
...
true
Source: Node.js API: --trace-sync-io
One readFileSync() produced four warnings in this run (observed: the top frames were openSync, tryStatSync, readSync and closeSync),
all pointing at the same line of your code, so read the first frame that belongs to you. Nothing was reported for the read before the
first turn.
--trace-exit and --trace-sigint
Two flags answer the questions "who is ending my process?" and "where is it stuck?". --trace-exit prints a stack trace whenever the
environment is exited proactively, that is by process.exit(); --trace-sigint prints the JavaScript stack when the process receives
SIGINT, even if the event loop is blocked.
// exit.js
import { setTimeout as sleep } from 'node:timers/promises';
function shutdownHelper(code) {
process.exit(code); // buried in "library" code
}
await sleep(10);
shutdownHelper(3);
(node:94914) WARNING: Exited the environment with code 3
at exit (node:internal/process/per_thread:241:13)
at shutdownHelper (file:///project/exit.js:5:11)
at file:///project/exit.js:9:1
Source: Node.js API: --trace-exit
// spin.js
function spin() { for (;;) { /* never yields */ } }
spin();
KEYBOARD_INTERRUPT: Script execution was interrupted by `SIGINT`
at spin (file:///project/spin.js:2:19)
at file:///project/spin.js:3:1
at run (node:internal/modules/esm/module_job:569:25)
For the shutdown side of this story see Process, Signals & Exit Codes.
Stack depth and other trace flags
--stack-trace-limit=<n> (or Error.stackTraceLimit in code) sets how many frames V8 keeps per error; the default is 10 and 0 disables
collection. On a 31-call recursive function, one run printed 10 frames by default and 35 (the 31 calls plus module-loading frames) with --stack-trace-limit=50, so raise it when the interesting
frame is deeper than the default shows (async stack traces through await are handled by V8 and need no flag).
| Flag | What it does |
|---|---|
|
Prints where |
|
Prints accesses to |
|
Prints TLS packet trace information to stderr, for connection problems; see HTTPS, TLS & HTTP/2. |
|
Present in |
|
Aborts instead of exiting so that the operating system writes a core file for post-mortem analysis with |
NODE_DEBUG and util.debuglog
NODE_DEBUG is a comma-separated list of core module names that should print internal debug information to stderr. The names are those of
the internal sections, such as fs, net, tls and http. The same mechanism
is available to your own code through util.debuglog(section), which returns a logging function that is a no-op unless the section name
appears in NODE_DEBUG. It supports a trailing * wildcard, and .enabled tells you whether the section is on, to guard expensive
formatting. The line is prefixed with the upper-cased section name and the process id.
// debuglog.js
import { createServer, get } from 'node:http';
import { debuglog } from 'node:util';
const log = debuglog('bookshelf');
const server = createServer((req, res) => {
log('request %s %s', req.method, req.url);
res.end('ok');
});
server.listen(0, '127.0.0.1', () => {
const { port } = server.address();
get({ port, host: '127.0.0.1', path: '/books' }, (res) => {
res.resume();
res.on('end', () => server.close());
});
});
$ node debuglog.js
$ NODE_DEBUG=bookshelf node debuglog.js
BOOKSHELF 7806: request GET /books
$ NODE_DEBUG='book*' node debuglog.js
BOOKSHELF 8676: request GET /books
$ NODE_DEBUG=http node debuglog.js
HTTP 7810: createConnection 127.0.0.1:50601:
HTTP 7810: http createConnection should use proxy for 127.0.0.1:50601: false
HTTP 7810: sockets 127.0.0.1:50601: 1 1
HTTP 7810: outgoing message end.
(node:7810) Warning: Setting the NODE_DEBUG environment variable to 'http' can expose sensitive data (such as passwords, tokens and authentication headers) in the resulting log.
...
Three things were observed beyond the documentation. First, NODE_DEBUG=http prints a warning that the log can contain passwords, tokens and
authentication headers, so never switch it on in a shared log pipeline. Second, NODE_DEBUG=http,net produced 57 lines for a single local
request, so combine sections sparingly. Third, the built-in fetch() is traced under the sections fetch and undici, not http: with
NODE_DEBUG=fetch a request logged "connecting", "sending request" and "received response to GET … HTTP 200" lines. NODE_DEBUG_NATIVE
is the documented C counterpart (a list of core C modules). These are diagnostic outputs, not an API, and their wording can change between releases.
Network inspection
Chrome DevTools has a Network panel, and Node.js can feed it. With --experimental-network-inspection (Stability 1, Experimental; added in
v22.6.0 and v20.18.0) the outgoing HTTP requests of the process are announced to the inspector as CDP Network.* events, so a connected DevTools
window lists requests, headers, timing and responses of your server’s calls to other services. The next example avoids the browser: it starts
a target with --inspect-wait and the flag, subscribes to Network.enable over the WebSocket, and prints the events that a Network panel
would draw. The target makes one fetch() to a local server.
// net-target.js
import { createServer } from 'node:http';
const server = createServer((req, res) => {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify({ title: 'Dune' }));
}).listen(0, '127.0.0.1', async () => {
const { port } = server.address();
const res = await fetch(`http://127.0.0.1:${port}/books/1`);
console.log(await res.json());
server.close();
});
// net-client.mjs
import { spawn } from 'node:child_process';
const child = spawn(
process.execPath,
['--inspect-wait=127.0.0.1:0', '--experimental-network-inspection', 'net-target.js'],
{ stdio: ['ignore', 'inherit', 'pipe'] },
);
const url = await new Promise((resolve) => {
child.stderr.setEncoding('utf8');
child.stderr.on('data', (t) => {
const m = /ws:\/\/\S+/.exec(t);
if (m) resolve(m[0]);
});
});
const ws = new WebSocket(url);
ws.onmessage = ({ data }) => {
const m = JSON.parse(data);
if (!m.method?.startsWith('Network.')) return;
const p = m.params;
const detail = p.request ? `${p.request.method} ${p.request.url.replace(/:\d+/, ':<port>')}`
: p.response ? `HTTP ${p.response.status} ${p.response.mimeType}`
: '';
console.log(m.method, detail);
};
await new Promise((resolve) => (ws.onopen = resolve));
ws.send(JSON.stringify({ id: 1, method: 'Network.enable' }));
ws.send(JSON.stringify({ id: 2, method: 'Runtime.runIfWaitingForDebugger' }));
child.on('exit', () => ws.close());
Network.requestWillBeSent GET http://127.0.0.1:<port>/books/1
Network.responseReceived HTTP 200 application/json
Network.loadingFinished
{ title: 'Dune' }
Source: Node.js API: --experimental-network-inspection, Node.js API: Integration with DevTools, Chrome DevTools: Network features reference
Two observations from running it. Without --experimental-network-inspection the same client received no Network.* events at all, so the flag
is required, and a request made with http.get() instead of fetch() produced the same three events, so both client APIs are covered. The
node:inspector module also exposes the other direction (Stability 1.1): inspector.Network.requestWillBeSent(), responseReceived(),
loadingFinished(), loadingFailed(), dataReceived() and dataSent() (and, since v24.7.0, WebSocket events) let a library broadcast
requests that Node.js itself does not instrument, such as those of a custom client, to every connected front end. For TLS problems
(--trace-tls) and for a proxy-based view of traffic, see Fetch & HTTP Clients and
HTTPS, TLS & HTTP/2.
Workers, child processes and other targets
A worker thread has its own V8 isolate, so it needs its own inspector. Node.js 24.1.0 and 22.17.0 added --experimental-worker-inspection
(Stability 1.1, Active development) so that DevTools can list and debug workers of a process started with --inspect; this is taken from
the CLI reference and was not run. A Session created in a worker can reach the main thread with connectToMainThread(). Child processes are
separate processes and so are separate targets: a flag put in NODE_OPTIONS reaches all of them, and a fixed port collides. With
NODE_OPTIONS='--inspect=127.0.0.1:9333' a parent that spawned one child printed "Debugger listening" for the parent and, for the child, "Starting inspector
on 127.0.0.1:9333 failed: address already in use" (observed on Node.js 26), so use --inspect-port=0 or give each process its own
port. The patterns are described in Worker Threads and Child Processes.
Debugging remotely and safely
The inspector is a remote-code-execution endpoint by design: a client that can reach it can evaluate arbitrary JavaScript with the privileges of the process. The Node.js documentation is blunt about this, and the rules that follow are the ones in the Learn guide and the CLI reference.
| Exposure | What to know |
|---|---|
Bound to a public address ( |
Any client that can reach the port can connect and run code. The CLI reference calls |
Bound to |
Still open to every local process and every local user that can open the port; the guide says this is by design, so that local debuggers can attach conveniently. |
A web page in a browser |
The same-origin policy blocks the initial HTTP request that obtains the session id, and Node.js checks that the |
Switched on later by a signal |
|
Under the permission model |
With |
Source: Node.js Learn: Debugging Node.js (security implications), Node.js API: binding the inspector to a public IP and port is insecure, Node.js API: --allow-inspector
The guidance that follows from this is short. Never bind the inspector to a public interface, and never leave --inspect in a production
start-up command, the NODE_OPTIONS of an image or a Kubernetes manifest. When you must debug something that is not on your machine, tunnel
to the loopback address of the remote host instead of opening the port; the Learn guide recommends an SSH tunnel and gives an example for
illustrative purposes. The command from the guide has this shape and was not run for this page, because the machine used here has no SSH server: ssh -L
9221:localhost:9229 user@remote.example.com, after which a local DevTools or VS Code attaches to localhost:9221, and the tunnel’s encryption
and authentication stand in front of the inspector. Attach by IP address or by localhost, since hostnames are rejected by the Host check.
Containers are the common case where the process must listen on 0.0.0.0 inside the container, because the container’s loopback is not
reachable from the host. The safe pattern is to let the process listen on all interfaces inside the container and publish the port only to the
host’s loopback address.
docker run -d --rm --name bookshelf-debug \
-p 127.0.0.1:9229:9229 \
-v "$PWD":/app:ro -w /app \
node:26-slim node --inspect=0.0.0.0:9229 hang.js
$ docker logs bookshelf-debug
Debugger listening on ws://0.0.0.0:9229/e4c00a80-7803-454c-984e-0f6c99698af0
For help, see: https://nodejs.org/learn/getting-started/debugging
$ docker port bookshelf-debug
9229/tcp -> 127.0.0.1:9229
$ curl -s http://127.0.0.1:9229/json/version
{
"Browser": "node.js/v26.10.0",
"Protocol-Version": "1.1"
}
Source: Node.js Learn: Debugging Node.js (enabling remote debugging scenarios), Docker: docker container run
The -p 127.0.0.1:9229:9229 form is what keeps the container’s debug port off the network; plain -p 9229:9229 publishes it on every host
interface. In VS Code, attach with the container configuration shown earlier (localRoot and remoteRoot map your files to /app). The
Docker side of this is covered in Debugging Containers, and NestJS projects have the same
workflow with framework-specific tips in NestJS Debugging & Devtools.
Production is a different problem
The Learn page on live debugging says that using a live debugger in production is usually not an option: you have limited access to the
machine and you cannot interrupt a process that handles business-critical work. Breakpoints stop the whole event loop, so a paused
production server stops serving every request. In production, collect evidence without stopping anything: structured logs with correlation
ids, a diagnostic report on failure, a CPU profile or heap snapshot taken on demand, and NODE_DEBUG or --trace-* flags on a single canary
instance. Those techniques are the subject of Diagnostic Reports, Logging & Observability,
Profiling & Performance and Production Deployment.
Probe mode, as noted, only launches a new process, so it is for reproducing a problem, not for inspecting the live one.
Common problems
| Symptom | Likely cause and fix |
|---|---|
"Starting inspector on 127.0.0.1:9229 failed: address already in use" |
Another process (or a child that inherited |
The debugger attaches but breakpoints never hit |
The code ran before you attached (use |
Breakpoints in a TypeScript file turn grey in VS Code |
The map does not match the output. Rebuild with |
DevTools lists the process, but the connection fails when using a hostname |
The |
Stack traces point into a bundle |
Run with |
A server pauses and stops answering when you attach |
Expected: a breakpoint stops the event loop. Use a logpoint or probe mode to observe without stopping, and do not attach to production. |
|
Nothing is listening on the port the client assumes. |
|
The runner forces a concurrency of 1 when the inspector is on; debug a single test file instead, or set |
What changed recently
| Version | Change |
|---|---|
v26.7.0 |
|
v26.6.0 (v24.20.0) |
|
v26.4.0 (v24.19.0) |
|
v26.1.0 (v24.16.0) |
Probe mode ( |
v25.5.0 |
|
v25.0.0 and v24.12.0 |
|
v24.8.0 and v22.20.0 |
|
v24.7.0 |
WebSocket events ( |
v24.5.0 and v22.19.0 |
|
v24.1.0 and v22.17.0 |
|
v23.7.0 and v22.14.0 |
|
v22.6.0 and v20.18.0 |
|
v22.2.0 and v20.15.0 |
|
Where the books are outdated
Two O’Reilly books touch this topic, and both predate parts of the current tooling. Efficient Node.js by Samer Buna (2025) introduces
node inspect, --inspect and --inspect-brk, attaching Chrome DevTools through chrome://inspect, the debugger; statement, and the list of
--trace-* flags printed by node --help, and also explains NODE_DEBUG. Node.js Projects by Jonathan Wexler (2025) covers the VS Code
debugger in an appendix, as an alternative to console.log(). They remain a sound starting point; these are the places where today’s behaviour
goes further, or where the habit they describe should change.
| Topic in the books | Current practice (checked 2026-10-04) |
|---|---|
Debugging starts with |
|
The |
Still correct, but start the inspector on port |
The |
The set is stable, but the listing includes |
|
Transpiled and bundled code is the norm: use |
|
It still works the same way, but |
VS Code debugging is a single launch button |
The debugger has auto attach, a JavaScript debug terminal, |
Process restarts use external watchers such as |
|
An external HTTP client (Postman) is used to exercise your own API |
That covers requests into your server. For requests that your process makes, |
References
-
Node.js Learn: Debugging Node.js, https://nodejs.org/learn/getting-started/debugging
-
Node.js Learn: Live Debugging, https://nodejs.org/learn/diagnostics/live-debugging
-
Node.js Learn: Using Inspector, https://nodejs.org/learn/diagnostics/live-debugging/using-inspector
-
Node.js Learn: Running TypeScript Natively, https://nodejs.org/learn/typescript/run-natively
-
Node.js API: Debugger, https://nodejs.org/docs/latest/api/debugger.html
-
Node.js API: Debugger, interactive mode, https://nodejs.org/docs/latest/api/debugger.html#interactive-mode
-
Node.js API: Debugger, probe mode, https://nodejs.org/docs/latest/api/debugger.html#probe-mode
-
Node.js API: V8 inspector integration for Node.js, https://nodejs.org/docs/latest/api/debugger.html#v8-inspector-integration-for-nodejs
-
Node.js 24 API: Debugger, https://nodejs.org/docs/latest-v24.x/api/debugger.html
-
Node.js API: Inspector, https://nodejs.org/docs/latest/api/inspector.html
-
Node.js API: inspector.open(), https://nodejs.org/docs/latest/api/inspector.html#inspectoropenport-host-wait
-
Node.js API: inspector.Session, https://nodejs.org/docs/latest/api/inspector.html#class-inspectorsession
-
Node.js API: Inspector, integration with DevTools, https://nodejs.org/docs/latest/api/inspector.html#integration-with-devtools
-
Node.js API: session.connectToMainThread(), https://nodejs.org/docs/latest/api/inspector.html#sessionconnecttomainthread
-
Node.js API: Command-line options, https://nodejs.org/docs/latest/api/cli.html
-
Node.js API: --inspect-brk, https://nodejs.org/docs/latest/api/cli.html#--inspect-brkhostport
-
Node.js API: --inspect-wait, https://nodejs.org/docs/latest/api/cli.html#--inspect-waithostport
-
Node.js API: --inspect-port, https://nodejs.org/docs/latest/api/cli.html#--inspect-porthostport
-
Node.js API: --inspect-publish-uid, https://nodejs.org/docs/latest/api/cli.html#--inspect-publish-uidstderrhttp
-
Node.js API: Binding the inspector to a public IP and port is insecure, https://nodejs.org/docs/latest/api/cli.html#warning-binding-inspector-to-a-public-ipport-combination-is-insecure
-
Node.js API: --allow-inspector, https://nodejs.org/docs/latest/api/cli.html#--allow-inspector
-
Node.js API: --enable-source-maps, https://nodejs.org/docs/latest/api/cli.html#--enable-source-maps
-
Node.js API: --experimental-network-inspection, https://nodejs.org/docs/latest/api/cli.html#--experimental-network-inspection
-
Node.js API: --experimental-worker-inspection, https://nodejs.org/docs/latest/api/cli.html#--experimental-worker-inspection
-
Node.js API: --trace-warnings, https://nodejs.org/docs/latest/api/cli.html#--trace-warnings
-
Node.js API: --trace-uncaught, https://nodejs.org/docs/latest/api/cli.html#--trace-uncaught
-
Node.js API: --trace-sync-io, https://nodejs.org/docs/latest/api/cli.html#--trace-sync-io
-
Node.js API: --trace-exit, https://nodejs.org/docs/latest/api/cli.html#--trace-exit
-
Node.js API: --trace-sigint, https://nodejs.org/docs/latest/api/cli.html#--trace-sigint
-
Node.js API: --trace-require-module, https://nodejs.org/docs/latest/api/cli.html#--trace-require-modulemode
-
Node.js API: --stack-trace-limit, https://nodejs.org/docs/latest/api/cli.html#--stack-trace-limitlimit
-
Node.js API: --abort-on-uncaught-exception, https://nodejs.org/docs/latest/api/cli.html#--abort-on-uncaught-exception
-
Node.js API: NODE_DEBUG, https://nodejs.org/docs/latest/api/cli.html#node_debugmodule
-
Node.js API: util.debuglog(), https://nodejs.org/docs/latest/api/util.html#utildebuglogsection-callback
-
Node.js API: process.debugPort, https://nodejs.org/docs/latest/api/process.html#processdebugport
-
Node.js API: module.setSourceMapsSupport(), https://nodejs.org/docs/latest/api/module.html#modulesetsourcemapssupportenabled-options
-
Node.js API: TypeScript, source maps, https://nodejs.org/docs/latest/api/typescript.html#source-maps
-
Node.js API: fetch(), https://nodejs.org/docs/latest/api/globals.html#fetch
-
Node.js API: Test runner, https://nodejs.org/docs/latest/api/test.html
-
Node.js API: Deprecations, DEP0005, https://nodejs.org/docs/latest/api/deprecations.html#dep0005-buffer-constructor
-
Chrome DevTools Protocol viewer, https://chromedevtools.github.io/devtools-protocol/
-
Chrome DevTools Protocol: Debugger domain, https://chromedevtools.github.io/devtools-protocol/tot/Debugger/
-
Chrome DevTools: Pause your code with breakpoints, https://developer.chrome.com/docs/devtools/javascript/breakpoints
-
Chrome DevTools: Network features reference, https://developer.chrome.com/docs/devtools/network
-
Visual Studio Code: Node.js debugging, https://code.visualstudio.com/docs/nodejs/nodejs-debugging
-
ECMA-426: Source map format, https://tc39.es/ecma426/
-
esbuild: API, https://esbuild.github.io/api/
-
esbuild: API, bundle, https://esbuild.github.io/api/#bundle
-
esbuild: API, sourcemap, https://esbuild.github.io/api/#sourcemap
-
Docker: docker container run, https://docs.docker.com/reference/cli/docker/container/run/
-
O’Reilly: Efficient Node.js, Samer Buna, https://www.oreilly.com/library/view/efficient-node-js/9781098145187
-
O’Reilly: Node.js Projects, Jonathan Wexler, https://www.oreilly.com/library/view/node-js-projects/9781098173135/