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.
Whether one agrees with these opinions or not, the result is the same: professional developers frequently spend enormous amounts of time diagnosing the debugging tools themselves rather than debugging their own applications.

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.

The effective fix: start VS Code with Node configured to prefer IPv4 when resolving 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"
    }
  ]
}
The 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

  1. Install dotnet-serve if needed.
  2. Put both JSON files under the project’s .vscode directory.
  3. Open the project folder in VS Code.
  4. Choose Chrome Explicit.
  5. Press F5.
F5 preLaunchTaskdotnet serve Problem matcherserver ready js-debuglaunch + attach Chromebreakpoint hit

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"
Mental model: 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.

Key distinction: 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

VS Code UITask Runnerjs-debugChrome Run preLaunchTask dotnet serve starts Watcher says ready Start debug configuration Launch Chrome with CDP port Discovery endpoint available Enable Runtime + Debugger Send user breakpoints through DAP Set breakpoint by URL Debugger.paused

Chrome may begin with about:blank. The debugger connects, enables CDP domains, installs breakpoints, then allows startup code to run.

Visual Studio Code — Run and Debug
8<script>9 const message = "Debugger attached";10 console.log(message);11 document.body.dataset.ready = "true";12</script>

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
  }
}
VS Code lines are one-based. CDP lines are zero-based. Editor line 10 becomes 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
Modern js-debug can create a parent browser session and child page sessions. A parent termination event does not automatically prove the page target died.

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
The key was comparing the endpoint js-debug actually used with the endpoint Chrome was truly listening on.

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

VS Code editor and Debug UI DAP JavaScript Debug Adapter (js-debug)translation, mapping, launch, target management CDP Chrome browser processTarget, Page, Runtime, Debugger domains Renderer process and V8 JavaScript engine

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
Do not expose the remote debugging port to untrusted networks. A CDP client can inspect and control the browser.

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

  1. Test /json/version.
  2. Test /json/list.
  3. Inspect the exact URL in the trace.
  4. 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

  1. Return to the last known-good configuration.
  2. Change one variable at a time.
  3. Find the last successful trace event.
  4. Validate the next boundary independently.
  5. Trust observed endpoints and command lines over assumptions.
Practice

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.

Source Mapping

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 }
Web Applications

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.

Common PWA trap: a stale service worker can serve older JavaScript even though the files on disk changed. Clear site data, unregister the worker, or use a fresh debugging profile when validating source changes.

Useful Chrome views

chrome://serviceworker-internals
chrome://inspect
DevTools → Application → Service Workers
DevTools → Application → Storage
Extension Development

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.

Hands-on Lab

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

  1. Find the last successful event.
  2. Identify the next expected event.
  3. Test that boundary independently.
  4. Do not infer that a later cleanup error caused the original failure.
  5. Compare against a known-good trace whenever possible.
Reference

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.
Index

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-chrome does not point to Chrome.exe.
  • isBackground does not hide a task.
  • problemMatcher is 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