Dev Tools

VSCode Debugging Primer: From launch.json to Hovering Over a Variable

Compared with sprinkling console.log everywhere, debugging lets you pause and inspect the call stack and every variable at each step. Starting from editing launch.json, this guide gets Node and front-end projects debugging, then walks through the sidebar, watch expressions and conditional breakpoints.

By LaoHand Team·7 min read·Updated 2026-09-06

What Breakpoint Debugging Is and How It Differs from console.log

console.log only prints where you anticipated, and every debug cycle means a full run, then re-editing and re-running. Breakpoint debugging pauses the program at the suspicious line, where you can inspect every in-scope variable, read the call stack and step line by line — without deciding in advance what to log.

It shines on complex logic: when a value hops across several functions, you can follow the stack to see where it came from and where it got overwritten. VSCode ships a built-in debugger, and Node, browser, Python and Go each have extensions, but the entry point is always the same .vscode/launch.json.

// .vscode/launch.json —— 最小可用配置
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "启动当前文件",
      "program": "${file}"
    }
  ]
}

Get Node and Front-End Debugging Running

The classic Node pitfall is "I set a breakpoint and it never fires". The cause is usually that the entry you launch is not the file you set the breakpoint on: launch the current file and point program at the right path, or configure your npm script as the launch target.

Front-end (Vite/Webpack) needs source maps so the debugger can map compiled code back to your sources. Using pwa-chrome, set the launch config to open a URL, which may combine a webServer target or simply attach to a running dev server. The difference between attach and launch: request: launch means the debugger spawns the process; attach connects to one already running.

{
  "type": "pwa-chrome",
  "request": "launch",
  "name": "调试 Vite",
  "url": "http://localhost:5173",
  "webRoot": "${workspaceFolder}/src",
  "sourceMapPathOverrides": {
    "webpack:///./src/*": "${webRoot}/*"
  }
}

Five Actions after Hitting a Breakpoint

After a breakpoint hits, the debug toolbar holds the key actions: Continue (F5) to the next breakpoint, Step Into (F11) to dive into a called function, Step Over (F10) to run the current line without entering functions, Step Out (Shift+F11) to finish the whole function and land back at the caller, plus restart/stop.

Knowing the difference is debugging core skill: Step Into drops you inside the current line’s function, ideal for investigating internal logic; Step Over runs the whole line at once, for viewing this layer’s flow; Step Out quickly ends the current function to see what it actually returns, then lands one level up.

To hop straight into a specific branch on a conditional, make the breakpoint conditional, or use Jump to Cursor (right-click) to move the execution pointer directly to the target line instead of pressing F5 repeatedly. You can remap these keys in the keyboard shortcuts panel, but the defaults already cover most scenarios.

F5            继续 / 开始调试
F10           迈过(不进入被调函数)
F11           单步进入(进入被调函数)
Shift + F11    跳出当前函数
Shift + F5     停止调试
右键行号 -> Add Conditional Breakpoint  条件断点

Sidebar, Variables and Watch: See Values Without Printing

The debug sidebar has three panels: Variables lists every in-scope variable with its current value, Watch lets you pin a few expressions you want to keep an eye on, and CALL STACK shows the call chain — the easiest way to find who changed a value that flows through function arguments.

In the Variables panel objects and arrays expand, and hovering over a variable in the source shows a tooltip preview. To observe derived values like a queue’s length or an intermediate computation, add an expression to Watch such as this.items.length, or use JSON for clarity. A conditional breakpoint (right-click the breakpoint → Conditions) pauses only when a condition holds, cutting down on needless interruptions.

# Watch 表达式示例(在 WATCH 面板逐个添加)
this.items.length
JSON.stringify(this.currentRow)
Date.now() - this.startedAt   # 计算已耗时
# 条件断点:右键断点 -> "条件",输入
row.status === "PENDING"

Four Checks for "Breakpoint Not Firing" and "Changes Not Applying"

The most common stalls: the current line never executes because the run went down another branch; source mapping fails and the breakpoint shows gray/hollow (not hit), meaning source map or webRoot is wrong; you changed code but the debugger still runs the old version, so restart the debug session if using -w; or a type/path casing mismatch breaks the mirror across files.

Self-check in order: is the breakpoint solid filled → is the debugged process freshly started → do the breakpoint file and program/webRoot share the same source → does DEBUG CONSOLE show any source-map error. Doing the first two resolves most "not working" cases.

# 强制刷新窗口并确认调试进程重启方式
# Node:启动命令确认 --inspect / --watch 行为
"runtimeArgs": ["--inspect=9229", "--watch"]
# 若断点为灰色空心,检查 webRoot 是否指向源码目录

Official References

Each command links to its official documentation below, so you can verify the latest usage and read deeper.