Docs · Inspector

Debugging with the inspector

Oam.js embeds the V8 Inspector and speaks the Chrome DevTools Protocol over a WebSocket, the same way Node does. Any CDP client attaches: Chrome DevTools, VS Code, or something you wrote.

Turning it on

oam run --inspect app.ts
oam run --inspect-brk app.ts        # wait for a debugger, break on line 1
oam serve --inspect server.ts
oam run --inspect=9230 app.ts       # a bare port binds 127.0.0.1
oam run --inspect=0.0.0.0:9229 app.ts
Flag What it does
--inspect[=[host:]port] Start the inspector and run the program immediately. A debugger can attach at any time. Default 127.0.0.1:9229.
--inspect-brk[=[host:]port] Start the inspector and execute nothing until a debugger attaches and sends Runtime.runIfWaitingForDebugger, then break on the first statement. Wins if both flags are given.

Both accept a bare port (--inspect=9230), which binds 127.0.0.1, or host:port. Anything else is rejected before the program starts, with oam run: invalid --inspect address '<value>' (expected [host:]port).

Executables built by oam compile accept the same two flags, with one difference: they parse their own argv so that everything else reaches your program, and there the value must be attached with =. ./app --inspect-brk=9230 works; ./app --inspect-brk 9230 starts the inspector on the default port and hands 9230 to your script.

oam test, oam check and oam repl have no --inspect flag, and reject it rather than ignoring it.

What it prints

Two lines on stderr, before your program produces anything:

Debugger listening on ws://127.0.0.1:9229/f314a409-397d-8046-f613-d63c27b9e2bd
For help, see: https://oamjs.org/docs/inspector

The first line is what tooling scrapes. The target id is fresh per process, so a wrapper script cannot cache the URL between runs — read it from stderr, or ask the discovery endpoint below.

Attaching

In Chrome, open chrome://inspect, add 127.0.0.1:9229 under "Discover network targets", and the target appears as oam. The inspector also answers the discovery endpoints a CDP client expects, on the same port:

$ curl -s http://127.0.0.1:9229/json/list
[{"description":"oam instance","id":"f314a409-...","title":"oam","type":"node",
  "webSocketDebuggerUrl":"ws://127.0.0.1:9229/f314a409-...",
  "devtoolsFrontendUrl":"devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:9229/f314a409-..."}]

$ curl -s http://127.0.0.1:9229/json/version
{"Browser":"oam/0.14.0","Protocol-Version":"1.3"}

/json and /json/list both return the single target; there is always exactly one, because oam runs one context per isolate.

One session at a time

A second client that tries to connect while another is attached is turned away with 503 Service Unavailable rather than silently sharing the session. Once the attached client disconnects the next one is accepted, for the life of the process — so reconnecting after closing DevTools works, but two debuggers at once do not.

If a client disappears while execution is paused at a breakpoint, the pause is released and the program runs on to completion. A vanished debugger cannot leave a process wedged.

TypeScript: what the debugger actually sees

Read this before debugging a .ts file. oam strips and transpiles TypeScript before V8 compiles it, and it does not hand the debugger a source map. Your breakpoints land in generated JavaScript.

The script V8 reports carries the path you ran — the .ts file, not a temporary — but its contents are the transpiled output, and sourceMapURL is empty. For this input:

interface Point {
  x: number;
  y: number;
}

function dist(p: Point): number {
  const total: number = p.x * p.x + p.y * p.y;
  return Math.sqrt(total);
}

console.log(dist({ x: 3, y: 4 }));

a debugger asking for the script source gets:

function dist(p) {
	const total = p.x * p.x + p.y * p.y;
	return Math.sqrt(total);
}
console.log(dist({
	x: 3,
	y: 4
}));

Types are gone, and the code has been reflowed — the console.log call is one line in the source and four in the output. So line numbers do not agree between your editor and the Sources panel, and a breakpoint set by line number in an external client can land somewhere you did not mean. Set breakpoints in the panel, on the code the panel is showing, or use a debugger; statement, which survives the transpile and stops in the right place every time.

Printed stack traces are a different story: those are mapped back, because oam keeps its own source-map registry for them. The same program throwing reports the .ts line you wrote:

Error: boom
    at dist (C:\path\to\boom.ts:8:9)
    at C:\path\to\boom.ts:12:13

The asymmetry is real and worth remembering: stack traces speak TypeScript, the debugger speaks JavaScript. Plain .js and .jsx entries are unaffected by any of this; nothing rewrites a .js file's statements.

Exposing the port is exposing the process

The Chrome DevTools Protocol has no authentication. Anything that can open that WebSocket can evaluate arbitrary code inside your process, read every variable, and reach whatever the process can reach. The default bind is 127.0.0.1 for that reason. Binding 0.0.0.0 on a shared or internet-facing host hands the process to the network; if you need to debug remotely, forward the port over SSH instead and leave the listener on loopback.

When it will not start

If the address is already in use, or the bind is refused, oam reports it as a diagnostic and exits rather than running your program without the debugger you asked for:

error[OAM-RT0004]: could not start inspector on 127.0.0.1:9229: <os error>

The usual cause is a previous --inspect run that has not exited. Pick another port with --inspect=9230, or find the process still holding 9229.

OAM-RT0004 is overloaded: the same code also means an unhandled promise rejection. Read the message, not the number. An executable built by oam compile reports the same failure without a code at all, as could not start inspector: <os error>.