Author's Preface
Auden Antony
Software Architect
Laveda Jones Productions
Who This Guide Is For
This guide is intended for professional software developers, software architects, technical leads, and younger developers who want to build strong debugging skills rather than relying on increasingly common trial-and-error techniques. My goal is not to criticize newer generations of developers, but to help them avoid losing engineering practices that have served the software industry for decades.
Why I Wrote This Guide
After more than 35 years as a software developer and architect, I have watched the industry change dramatically. Many changes have been exciting and productive. Others, in my opinion, have made professional software development more difficult than it needs to be.
- Companies rapidly returning to outsourcing without fully addressing previous engineering lessons.
- Organizations placing less emphasis on experienced developers.
- Rapid AI adoption without ensuring developers understand, maintain, or troubleshoot generated code.
- A noticeable decline in traditional debugging skills, often replaced by
console.log()debugging.
Professional debugging should include breakpoints, stepping, watches, conditional breakpoints, asynchronous debugging, loop analysis, and understanding call stacks.
I do not blame younger developers. In many cases, the available documentation and support systems simply do not teach these concepts effectively.
My Frustrations with the Current State of Tooling
- Documentation often feels fragmented or incomplete.
- Rapid release cycles quickly obsolete documentation.
- Developers increasingly rely on inconsistent AI-generated answers.
- Community support often focuses more on process than engineering problems.
- Smaller organizations can spend weeks diagnosing issues that never receive official attention.
Adapting to a New Development World
I have had to reinvent portions of my skill set by embracing Visual Studio Code, TypeScript, Node.js, modern front-end frameworks, SPAs, and PWAs.
Why This Guide Exists
I repeatedly built professional debugging environments that worked perfectly until an update to VS Code, js-debug, Chrome, Chromium, Node.js, DAP, CDP, or even launch.json broke them. Searching the web often produced advice centered around console.log(), while AI assistants frequently circled without understanding the underlying architecture.
Eventually I stopped treating browser debugging as magic and learned the underlying protocols—DAP, CDP, breakpoint transmission, browser targets, and launch sequencing. This guide documents that journey in the hope of saving other developers days or weeks of frustration.
1. The Final Working Solution
Start with the end because this is the part a developer needs when F5 should simply work. In the case documented here, Chrome launched correctly and exposed the DevTools endpoint on IPv4 at 127.0.0.1:9222, while the VS Code JavaScript debugger attempted to contact IPv6 localhost at [::1]:9222.
localhost.set NODE_OPTIONS=--dns-result-order=ipv4first
code
To persist that setting for future processes on Windows:
setx NODE_OPTIONS "--dns-result-order=ipv4first"
After using setx, fully close and reopen VS Code. The setting applies only to processes started afterward.
Final tasks.json
Place this file in .vscode/tasks.json. Its job is to start a local HTTP server and tell VS Code when that server is ready.
{
"version": "2.0.0",
"tasks": [
{
"label": "Start HTTP Server",
"type": "shell",
"command": "dotnet serve --port 8000",
"options": {
"cwd": "${workspaceFolder}",
"shell": {
"executable": "cmd.exe",
"args": ["/c"]
}
},
"isBackground": true,
"problemMatcher": {
"owner": "dotnet",
"fileLocation": "relative",
"pattern": {
"regexp": "^([^\\s].*)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\):\\s+(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$",
"file": 1,
"location": 2,
"severity": 3,
"code": 4,
"message": 5
},
"background": {
"activeOnStart": true,
"beginsPattern": "Listening on:",
"endsPattern": "^\\s+https?://.+$"
}
}
}
]
}
Final launch.json
Place this file in .vscode/launch.json. Its job is to tell VS Code which debug adapter to use, which browser to launch, which URL to open, and how to map browser URLs back to local files.
{
"version": "0.2.0",
"configurations": [
{
"name": "Chrome Explicit",
"type": "pwa-chrome",
"request": "launch",
"runtimeExecutable": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
"address": "127.0.0.1",
"port": 9222,
"userDataDir": "C:\\ChromeVSCodeDebug",
"url": "http://localhost:8000/index.html",
"webRoot": "${workspaceFolder}",
"trace": true,
"preLaunchTask": "Start HTTP Server"
}
]
}
address setting was correct, but the debugger’s launch path still resolved an internal localhost lookup to IPv6. The environment variable fixed the DNS ordering used by the Node.js runtime hosting js-debug.How to use it
- Install
dotnet-serveif needed. - Put both JSON files under the project’s
.vscodedirectory. - Open the project folder in VS Code.
- Choose Chrome Explicit.
- Press F5.
2. The Two Files You Need
Browser debugging in VS Code is split between two independent subsystems.
tasks.json
Controls supporting commands: build, serve, compile, watch, copy files, generate assets, or start a backend.
It belongs to the VS Code task runner.
launch.json
Controls the debug session: debugger type, launch versus attach, browser executable, URL, profile, port, and source mapping.
It belongs to the VS Code debug system.
The bridge is:
"preLaunchTask": "Start HTTP Server"
tasks.json prepares the environment. launch.json starts or attaches the debugger.3. Understanding tasks.json
Why a web page needs a server
A static HTML file can be opened with a file:// URL, but modules, service workers, fetch, CORS behavior, PWA installation, caching, and origin-based APIs work more realistically through HTTP.
Top-level structure
{
"version": "2.0.0",
"tasks": [
{ ... }
]
}
| Property | Meaning |
|---|---|
version |
The task schema version. |
tasks |
An array because a workspace may contain many tasks. |
The task label
"label": "Start HTTP Server"
The label is the exact identity referenced by preLaunchTask.
Shell task
"type": "shell"
A shell task runs through cmd.exe, PowerShell, Bash, or another shell. A process task launches an executable directly.
The command
"command": "dotnet serve --port 8000"
This starts a long-lived HTTP server.
Working directory
"cwd": "${workspaceFolder}"
The server serves the opened workspace rather than an inherited current directory.
isBackground
"isBackground": true
This does not hide the task. It tells the scheduler that the process is expected to stay alive, so readiness must be determined from output instead of process exit.
Why it is called a problem matcher
The feature originally parsed compiler output for the Problems panel. VS Code later reused the same output-watching engine to recognize lifecycle text from background processes.
The background watcher
"background": {
"activeOnStart": true,
"beginsPattern": "Listening on:",
"endsPattern": "^\\s+https?://.+$"
}
The server emits two lines:
Listening on:
http://localhost:8000
VS Code evaluates each line separately.
| Setting | Meaning |
|---|---|
activeOnStart |
Assume startup work is active immediately. |
beginsPattern |
Marks the start of a busy cycle. |
endsPattern |
Marks readiness so dependent operations may continue. |
dotnet serve starts
↓
output: Listening on:
↓
output: http://localhost:8000
↓
endsPattern matches
↓
debugger launch continues
4. Understanding launch.json
name
"name": "Chrome Explicit"
The Run and Debug dropdown label.
type
"type": "pwa-chrome"
This is the debug adapter identifier, not Chrome.exe. The built-in JavaScript debugger recognizes this type and uses Chrome-oriented behavior.
chrome.exe is the browser. pwa-chrome is the debug adapter type. CDP is the protocol between the adapter and Chrome.Launch versus attach
request: launch
VS Code starts Chrome, connects, installs breakpoints, then navigates.
request: attach
Chrome is already running with remote debugging enabled.
runtimeExecutable
"runtimeExecutable": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
Removes ambiguity about which Chrome installation to use.
address and port
"address": "127.0.0.1",
"port": 9222
The DevTools endpoint begins as HTTP discovery and upgrades to WebSocket control. Port 9222 is conventional, not mandatory.
userDataDir
"userDataDir": "C:\\ChromeVSCodeDebug"
An isolated Chrome profile avoids collisions with the everyday browser profile.
url
"url": "http://localhost:8000/index.html"
The application URL.
webRoot
"webRoot": "${workspaceFolder}"
Maps browser URLs to local files.
http://localhost:8000/index.html
↓
C:\Project\index.html
trace
"trace": true
Enables DAP, CDP, target, breakpoint, and launch diagnostics.
preLaunchTask
"preLaunchTask": "Start HTTP Server"
Coordinates the task runner and debugger.
5. What Happens When You Press F5
Chrome may begin with about:blank. The debugger connects, enables CDP domains, installs breakpoints, then allows startup code to run.
Illustrative embedded screenshot mockup: a verified breakpoint with the execution pointer on line 10.
6. How a VS Code Breakpoint Becomes a Chrome Breakpoint
DAP request from VS Code
{
"command": "setBreakpoints",
"arguments": {
"source": { "path": "C:\\Project\\index.html" },
"breakpoints": [
{ "line": 10 },
{ "line": 12 }
]
}
}
CDP request to Chrome
{
"id": 1023,
"method": "Debugger.setBreakpointByUrl",
"params": {
"urlRegex": "...localhost:8000...index.html...",
"lineNumber": 9,
"columnNumber": 0
}
}
lineNumber: 9.Why setBreakpointByUrl
Debugger.setBreakpoint requires a scriptId, which exists only after parsing. By URL, the adapter can install a deferred breakpoint before the script loads.
Resolution and pause
{
"method": "Debugger.breakpointResolved",
"params": {
"location": {
"scriptId": "84",
"lineNumber": 9,
"columnNumber": 10
}
}
}
{
"method": "Debugger.paused",
"params": {
"hitBreakpoints": ["2:9:0:..."]
}
}
The adapter converts that into a DAP stopped event for the VS Code UI.
7. Creating and Reading js-debug Trace Logs
Enable tracing
"trace": true
The Debug Console prints a path similar to:
C:\Users\YourName\AppData\Roaming\Code\logs\...\ms-vscode.js-debug\vscode-debugadapter-xxxx.json
Important tags
| Tag | Meaning |
|---|---|
dap.receive |
VS Code sent a DAP request to the adapter. |
dap.send |
The adapter replied or emitted a DAP event. |
cdp.send |
The adapter sent a CDP command to Chrome. |
cdp.receive |
Chrome sent a CDP response or event. |
runtime.launch |
Browser launch and discovery activity. |
runtime.exception |
An internal operation failed. |
Read chronologically
runtime.launch: Launching Chrome
runtime.launch: Error looking up /json/version
runtime.exception: taskkill process not found
runtime.launch: Unable to launch browser
The final line is the symptom. The first failed boundary is the important one.
Healthy landmarks
Target.attachToBrowserTarget
Target.setDiscoverTargets
Target.targetCreated
Target.attachToTarget
Runtime.enable
Debugger.enable
Debugger.setBreakpointByUrl
Debugger.breakpointResolved
Debugger.paused
DAP event: stopped
Failure landmarks
Connection closed
Target.targetDestroyed
Target.detachedFromTarget
event: terminated
command: disconnect
Error looking up /json/version
Could not find any debuggable target
8. The Real-World Failure Investigation
Symptom
Unable to launch browser:
Could not connect to debug target at http://localhost:9222:
Could not find any debuggable target
What the successful trace proved
- The adapter initialized.
- Chrome was discovered.
- A page target was attached.
- Breakpoints were translated to CDP.
- Chrome resolved the breakpoints.
- Chrome emitted
Debugger.paused. - VS Code received DAP
stopped.
Therefore DAP, CDP, mapping, and breakpoint transmission were fundamentally sound.
The decisive line
Error looking up /json/version
url: http://[::1]:9222/json/version
The configuration explicitly requested IPv4, but the launch-time discovery lookup used IPv6 localhost.
Independent verification
curl -g "http://[::1]:9222/json/version"
curl: (7) Failed to connect to ::1 port 9222
curl http://127.0.0.1:9222/json/version
{
"Browser": "Chrome/151.0.7922.76",
"Protocol-Version": "1.3",
"webSocketDebuggerUrl":
"ws://127.0.0.1:9222/devtools/browser/..."
}
The workaround
set NODE_OPTIONS=--dns-result-order=ipv4first
code
Why this became confusing
- Generic errors collapsed many possible causes into “unable to attach.”
- Chrome’s multiple processes made PID evidence ambiguous.
- Launch mode combines process creation and protocol attachment.
- Changing several variables at once obscured cause and effect.
9. DAP and CDP Under the Hood
DAP
DAP standardizes how an editor talks to a debugger.
initialize
launch
attach
setBreakpoints
configurationDone
threads
stackTrace
scopes
variables
continue
next
stepIn
evaluate
disconnect
CDP
| Domain | Purpose |
|---|---|
Target |
Discover and attach to browser, page, and worker targets. |
Page |
Navigate, inspect frames, reload, lifecycle events. |
Runtime |
Evaluate JavaScript and inspect objects. |
Debugger |
Breakpoints, pause, resume, step, call frames. |
Network |
Requests, responses, cache, failures. |
HTTP discovery, then WebSocket
GET /json/version
GET /json/list
ws://127.0.0.1:9222/devtools/browser/<id>
ws://127.0.0.1:9222/devtools/page/<id>
10. Chrome’s Process Model and Debug Port
Browser process
├── GPU process
├── Network service
├── Storage service
├── Renderer process for page A
├── Renderer process for page B
└── Crashpad handler
| Switch | Purpose |
|---|---|
--remote-debugging-port=9222 |
Starts the DevTools endpoint. |
--remote-debugging-address=127.0.0.1 |
Binds to IPv4 loopback. |
--user-data-dir=C:\ChromeVSCodeDebug |
Uses an isolated profile. |
about:blank |
Neutral initial target while the debugger prepares. |
Useful diagnostics
curl http://127.0.0.1:9222/json/version
curl http://127.0.0.1:9222/json/list
netstat -ano | findstr :9222
tasklist | findstr /I chrome
wmic process where "name='chrome.exe'" get ProcessId,ParentProcessId,CommandLine
11. Troubleshooting Decision Tree
F5 waits forever
- Check the task terminal.
- Verify readiness text.
- Test begin and end patterns against individual lines.
- Confirm
isBackground: true.
Chrome does not launch
- Verify
runtimeExecutable. - Run Chrome manually.
- Check profile permissions and ProcessSingleton errors.
- Enable tracing.
Chrome launches but cannot attach
- Test
/json/version. - Test
/json/list. - Inspect the exact URL in the trace.
- Compare IPv4 and IPv6.
curl http://127.0.0.1:9222/json/version
curl -g "http://[::1]:9222/json/version"
Breakpoints are unbound
- Verify the expected file loaded.
- Check
webRoot. - Look for
Debugger.scriptParsed. - Look for
Debugger.breakpointResolved.
Breakpoints hit only after reload
The page likely executed before attachment. A proper launch workflow installs breakpoints before startup code runs.
Evidence-first checklist
- Return to the last known-good configuration.
- Change one variable at a time.
- Find the last successful trace event.
- Validate the next boundary independently.
- Trust observed endpoints and command lines over assumptions.
12. Professional Debugging Beyond console.log()
Logging is useful, but it is not a substitute for an interactive debugger. A professional workflow combines both tools deliberately.
| Technique | Best use | Limitation |
|---|---|---|
| Breakpoint | Pause at a precise execution point and inspect state. | Can alter timing in highly concurrent code. |
| Conditional breakpoint | Pause only when a data condition is true. | A costly condition can slow execution. |
| Hit-count breakpoint | Pause on a particular iteration or occurrence. | Counts can change when code paths change. |
| Logpoint | Emit information without editing source code. | Still requires a functioning debugger attachment. |
| Watch expression | Continuously evaluate important state while paused. | Evaluation may invoke getters or have side effects. |
console.log() |
Persistent event history and timing-sensitive observation. | Requires editing, rerunning, and mentally reconstructing state. |
Debugging asynchronous JavaScript
Promises, timers, DOM events, network callbacks, and async/await break the simple mental model of a single uninterrupted call stack. Modern debuggers preserve async ancestry so a paused callback can still show the chain that scheduled it.
Advanced: what an async stack represents
An async stack is not necessarily one physical V8 call stack. The debugger records scheduling relationships and presents a logical chain. This is why enabling async call-stack depth appears in CDP traces as Debugger.setAsyncCallStackDepth.
Loops and long-running processes
for (let i = 0; i < records.length; i++) {
process(records[i]);
}
Instead of adding repeated logging, use a conditional breakpoint such as i === 487, a hit count, or a condition based on the problematic record’s identifier.
13. Files, URLs, Inline Scripts, and Source Maps
The debugger must reconcile at least three identities:
Local source:
C:\Project\src\app.ts
Generated browser resource:
http://localhost:8000/dist/app.js
Chrome script:
scriptId = 84
webRoot, source maps, URL regular expressions, and script-parsed events are the bridge.
Why a breakpoint can be unbound
- The script has not loaded yet.
- The browser URL does not map to the local path.
- A source map is missing, stale, or points to a different tree.
- The selected line contains no executable statement.
- An inline script starts partway through an HTML file, changing script-relative offsets.
Trace evidence
Debugger.scriptParsed
Debugger.setBreakpointByUrl
Debugger.breakpointResolved
DAP event: breakpoint { verified: true }
14. Debugging PWAs, Workers, and Service Workers
A PWA can involve several independently debuggable targets:
- The visible page.
- A service worker controlling fetch and caching.
- Dedicated or shared web workers.
- Frames and embedded documents.
The Target CDP domain discovers these objects. Auto-attach settings determine whether the debugger pauses and configures newly created targets.
Useful Chrome views
chrome://serviceworker-internals
chrome://inspect
DevTools → Application → Service Workers
DevTools → Application → Storage
15. Writing a Minimal VS Code Debugger Adapter
A debugger extension is a translator and lifecycle manager. On one side it speaks DAP to VS Code. On the other side it speaks the target runtime’s native protocol—in this case CDP.
Extension registration
{
"contributes": {
"debuggers": [
{
"type": "my-chrome",
"label": "My Chrome Debugger",
"languages": ["javascript", "typescript"]
}
]
}
}
A user could then select:
{
"type": "my-chrome",
"request": "launch",
"name": "My Chrome Session"
}
Minimum DAP lifecycle
initialize
launch or attach
setBreakpoints
setExceptionBreakpoints
configurationDone
threads
stackTrace
scopes
variables
continue / next / stepIn
evaluate
disconnect
Minimum CDP lifecycle
GET /json/version
WebSocket connect
Target.setDiscoverTargets
Target.attachToTarget
Runtime.enable
Debugger.enable
Debugger.setBreakpointByUrl
Debugger.paused
Debugger.resume
Runtime.evaluate
Core translation example
// Conceptual TypeScript
async function setBreakpoints(sourcePath, dapBreakpoints) {
const urlRegex = localPathToUrlRegex(sourcePath);
return Promise.all(dapBreakpoints.map(async bp => {
const result = await cdp.send("Debugger.setBreakpointByUrl", {
urlRegex,
lineNumber: bp.line - 1,
columnNumber: (bp.column ?? 1) - 1
});
return {
verified: result.locations.length > 0,
line: bp.line
};
}));
}
Advanced: why a production debugger becomes complicated
A mature adapter must handle source maps, target replacement during navigation, workers, multiple sessions, remote objects, variable-reference lifetimes, exception policies, async stacks, browser discovery, profile management, process ownership, restarts, logpoints, function breakpoints, content validation, and version differences. The basic protocol is simple; reliable lifecycle management is the hard part.
16. Reading a Trace Like a Packet Capture
Use a four-column worksheet:
| Time | Layer | Event | Conclusion |
|---|---|---|---|
| 15:26:18.944 | Runtime | Chrome launch requested | The adapter reached browser startup. |
| 15:26:19.080 | HTTP discovery | http://[::1]:9222/json/version failed |
The first broken boundary is endpoint discovery. |
| Manual test | Network | 127.0.0.1:9222 works |
Chrome and CDP are healthy on IPv4. |
| Manual test | Network | [::1]:9222 fails |
The failure is an address-family mismatch. |
Method
- Find the last successful event.
- Identify the next expected event.
- Test that boundary independently.
- Do not infer that a later cleanup error caused the original failure.
- Compare against a known-good trace whenever possible.
17. Glossary
- DAP
- Debug Adapter Protocol—the editor-facing protocol used by VS Code.
- CDP
- Chrome DevTools Protocol—the browser-facing protocol used to control Chrome and V8.
- Debug adapter
- A process or extension component translating DAP into a runtime-specific debugging protocol.
- Target
- A debuggable Chrome entity such as a browser, page, frame, worker, or service worker.
- Execution context
- A JavaScript global environment in which expressions can run.
- Script ID
- A Chrome-generated identifier for a parsed script.
- Bound breakpoint
- A breakpoint resolved to an executable Chrome script location.
- Unbound breakpoint
- A requested breakpoint not yet matched to a loaded executable location.
- Problem matcher
- A VS Code output parser used for both diagnostics and background-task lifecycle signaling.
- Remote object
- A CDP reference to a value or object living inside the debuggee, rather than copied directly into the debugger.
18. Searchable Index
19. Reference Appendix
Minimal beginner launch configuration
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Web Page",
"type": "pwa-chrome",
"request": "launch",
"url": "http://localhost:8000/index.html",
"webRoot": "${workspaceFolder}",
"preLaunchTask": "Start HTTP Server"
}
]
}
Manual CDP launch
"C:\Program Files\Google\Chrome\Application\chrome.exe" ^
--remote-debugging-port=9222 ^
--remote-debugging-address=127.0.0.1 ^
--user-data-dir=C:\ChromeVSCodeDebug ^
http://localhost:8000/index.html
DAP-to-CDP translation
| User action | DAP | CDP |
|---|---|---|
| Add breakpoint | setBreakpoints |
Debugger.setBreakpointByUrl |
| Continue | continue |
Debugger.resume |
| Step over | next |
Debugger.stepOver |
| Evaluate | evaluate |
Runtime.evaluate |
| Call stack | stackTrace |
Data from Debugger.paused |
Common misconceptions
pwa-chromedoes not point to Chrome.exe.isBackgrounddoes not hide a task.problemMatcheris not limited to compiler errors.- Port 9222 is conventional, not magical.
- A Chrome window appearing does not prove the debugger connected.
- A missing launcher PID does not prove the browser died.
Final mental model
tasks.json
prepares the environment
↓
launch.json
selects and configures the debugger
↓
DAP
carries editor/debugger requests
↓
js-debug
translates paths, breakpoints, and lifecycle
↓
CDP
controls Chrome and V8
↓
Chrome target
executes and pauses your code