Debug Like Reacher: The Complete VS Code Case File
Rule one: don’t theorize ahead of the evidence
Reacher walks into a room he’s never seen before, looks at exactly three things, and tells you who did it, in what order, and why. Nobody believes him at first. Then it checks out, every time. Ask him how, and he gives you some version of the same answer: “I don’t guess. I get the facts, and then I know.”
Most developers debug the opposite way. Something’s broken, so they guess what might be wrong, drop a console.log next to that guess, rerun the app, squint at the terminal, and if the guess was wrong — which it usually is — they guess again. That’s not investigation. That’s theorizing before you have the facts and hoping you get lucky. Reacher would not work a case this way, and you shouldn’t debug one this way either.
A debugger is the alternative. You walk into the exact moment something goes wrong, freeze the scene, and look at everything that’s actually true right now — every variable, every call frame, every closure — instead of the one narrow fact you remembered to print in advance. You can also test a theory on the spot: change a value mid-scene and watch what happens next, live, instead of re-running the whole case from the start.
This is the most underused skill in a working developer’s toolkit. Almost nobody is taught to do it properly. This is the full case file — nothing skipped, nothing simplified away.
Chapter 1 — How a crime scene actually works
Before any VS Code specifics, you need the four facts every investigation runs on. Get these wrong and nothing below will make sense; get them right and every debugger in every language — not just VS Code’s — will feel familiar.
- Breakpoint — the moment you walk in and freeze the room. Nothing moves past this line until you say so.
- Call stack — the timeline of how everyone got here, most recent event first. This is the chain of custody you’d otherwise have to reconstruct by hand from scattered
console.loglines. - Scope / Variables — everything actually present in the room right now: locals, closures, globals, all with their real, current values. Not what you assumed was there. What’s actually there.
- Stepping — once the scene is frozen, you control the pace. Nothing proceeds until you decide it does.
Everything VS Code gives you is a UI wrapped around these four facts.
Chapter 2 — Setting up your field kit
Open the Run and Debug view (Ctrl+Shift+D / Cmd+Shift+D), then “create a launch.json file.” This file, at .vscode/launch.json, is your field kit: it tells VS Code exactly how to walk into a given scene — launch a fresh process, or quietly attach to one already in progress.
Walking in from the start: a plain Node.js app
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug server.js",
"program": "${workspaceFolder}/server.js",
"skipFiles": ["<node_internals>/**"]
}
]
}
skipFiles is you refusing to waste time interviewing bystanders — it keeps Node’s internal machinery out of your call stack. Without it, stepping “into” a function sometimes drags you through pages of node:internal noise that has nothing to do with your case.
Attaching mid-investigation
Reacher doesn’t always arrive at hour zero. Sometimes the scene’s already running — started by pnpm dev, nodemon, or sitting inside a Docker container — and you don’t want VS Code taking over its lifecycle, just observing it.
{
"type": "node",
"request": "attach",
"name": "Attach to Node",
"port": 9229,
"restart": true,
"skipFiles": ["<node_internals>/**"]
}
Start the app with node --inspect server.js (or --inspect-brk to freeze on the very first line, before anything runs), then hit “Attach to Node.” "restart": true means if the process dies and comes back, VS Code quietly reattaches instead of making you do it by hand.
The off-the-books version — if you don’t even want to write a config, run Debug: Toggle Auto Attach from the Command Palette and set it to smart. Any node process you now launch from VS Code’s integrated terminal gets a debugger attached automatically, no launch.json required. Useful for a quick, unplanned look at a scene.
Walking into the browser, from VS Code
This is the one most people never set up, then spend years wondering why they’re stuck console.log-ing React forever. VS Code’s built-in js-debug can launch and drive Chrome directly:
{
"type": "chrome",
"request": "launch",
"name": "Debug React app in Chrome",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/src",
"sourceMapPathOverrides": {
"webpack:///src/*": "${webRoot}/*"
}
}
Run this instead of manually opening Chrome yourself. Breakpoints you set inside your .tsx files in VS Code now bind directly in the live browser runtime — no DevTools tab, no window-switching, no translating a line number in your head.
One case, two scenes at once: a Next.js app
Next.js runs a Node server and ships client JS — two scenes, one case. You want both frozen and inspectable together. Use a compound config:
{
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Next.js: server-side",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["dev"],
"console": "integratedTerminal"
},
{
"type": "chrome",
"request": "launch",
"name": "Next.js: client-side",
"url": "http://localhost:3000"
}
],
"compounds": [
{
"name": "Next.js: full stack",
"configurations": ["Next.js: server-side", "Next.js: client-side"]
}
]
}
Launch “Next.js: full stack” and a breakpoint in a server component and one in a client component both work, in the same session — one case file covering both jurisdictions.
The shortcut version of the same idea — if you’d rather not maintain two separate configs, serverReadyAction lets one launch config watch your dev server’s own stdout and open the browser debugger the moment it says it’s ready:
{
"type": "node",
"request": "launch",
"name": "Launch dev server + attach Chrome",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["dev"],
"serverReadyAction": {
"pattern": "ready - started server on .+, url: (https?://.+)",
"uriFormat": "%s",
"action": "debugWithChrome"
}
}
One “Start” press, both ends of the case covered, no compound needed.
Interviewing a witness in isolation: debugging a Jest test
{
"type": "node",
"request": "launch",
"name": "Debug current Jest test",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["--runInBand", "--watchAll=false", "${fileBasenameNoExtension}"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
Set a breakpoint inside the component or the test, run this config with that test file focused, and you’re frozen inside your own test — far faster than reading a failed assertion off stdout and guessing backward from there.
Chapter 3 — Five ways to freeze a scene
Anyone can click the gutter to set a basic breakpoint. Reacher wouldn’t interview every single person at a packed stadium just because one of them is the culprit — he narrows it down first. These five breakpoint types are how you narrow it down too. (In current VS Code, right-clicking the gutter opens one “Add Conditional Breakpoint” entry with a dropdown for Expression vs Hit Count — they’re two faces of the same tool.)
1. Line breakpoint — the basic freeze. Click left of the line number.
2. Conditional breakpoint (Expression) — only pauses when an expression is true.
items[i].id === 'c-482' && items[i].status === 'pending'
This is how you work a loop over 10,000 items without interviewing 9,999 people who have nothing to do with the case.
3. Conditional breakpoint (Hit Count) — pause only on the Nth hit, e.g. >= 50. You already know the pattern repeats; you just need to be standing there the 50th time it does.
4. Logpoint — right-click the gutter → “Add Logpoint.” Prints a message to the Debug Console without freezing the scene and without touching your source code.
Rendering item {item.id} with count {renderCount}
This is a wire tap, not an arrest. Strictly better than console.log for exploratory work: no commit diff, no “whoops, left that in,” and you can add or remove the tap while the app keeps running.
5. Exception breakpoint — in the Breakpoints panel, check “Uncaught Exceptions” (and/or “Caught Exceptions”). You catch the moment of the act itself, with the real call stack intact, instead of showing up after the body’s already been found and trying to reconstruct what happened from a cold stack trace.
Chapter 4 — The interrogation room
- Variables — the live state at the frozen line. Expand objects, see array contents, see
this. Right-click any value → “Copy Value,” or “Set Value” — yes, you can rewrite a variable mid-scene and resume with the new value in play. This turns “what if X had actually been Y” from a theory into a five-second test, the same way Reacher tests an alibi by poking one hole in it and watching whether the whole story collapses. - Watch — pin an expression, not just a bare variable — any JS expression, like
items.filter(i => i.done).length— so it recalculates every time you freeze the scene. This is the one detail you keep asking about every single time you walk back into the room. - Call Stack — click any frame to jump your Variables and Debug Console context to that frame, even though execution is frozen deeper in. This is how you answer “wait, who actually called this with a bad argument” without adding a single log line — you cross-reference the timeline directly instead of asking witnesses to repeat their story.
- Debug Console — a live REPL scoped to wherever you’re frozen. Call your own functions, mutate
state, runJSON.stringify(response, null, 2)on something you’re inspecting. This is the most underused panel in the entire tool, and it’s where you actually interrogate the evidence instead of just looking at it.
Chapter 5 — No wasted motion
Reacher doesn’t search every room in a building when he already knows which one matters. Stepping works the same way — it’s about not descending into everything just because you can.
- Continue (F5) — run until the next frozen moment.
- Step Over (F10) — run the current line without descending into the function calls it makes. Use this most of the time — you don’t need to interview every bystander a line happens to brush past.
- Step Into (F11) — descend into the function call on this line. Use this only when you specifically suspect the case leads through that door.
- Step Out (Shift+F11) — finish the current function and surface back in its caller. Use this when you stepped in by mistake, or you’ve already learned what this room had to offer.
- Restart (Ctrl+Shift+F5) — rerun from the top with the same breakpoints already set. Faster than closing the case and opening a new one.
The actual skill, the one that separates someone competent from someone genuinely good: state your theory before you check it. Before pressing Step Over, say — out loud or in your head — what you expect the next line to do and what you expect to still be true after. If the Variables panel agrees, your model of the case is solid. If it doesn’t, you just found the exact point where your theory and reality split — which is the only place a real bug ever hides. (This is the same discipline from tracing large codebases — predict, then verify. A debugger is just the sharpest instrument for running that loop.)
Chapter 6 — Working a React cold case
Freezing the scene inside a hook. A breakpoint inside a useEffect body shows you exactly what triggered it and what the closed-over values actually were at that render:
useEffect(() => {
// breakpoint here — check the Variables panel for `userId`, `filters`
fetchResults(userId, filters);
}, [userId, filters]);
Then check the Call Stack — React’s own internals will be sitting in there, but so is your calling component, which tells you which render actually triggered the effect. That’s the difference between knowing an effect ran and knowing why.
Catching a repeat offender: the unexplained re-render. A logpoint at the top of a component body tells you how many times it rendered without touching the component’s code at all:
{Date.now()} rendering <ProductCard id={id} />
Pair it with a Hit Count condition (renderCount > 20) to catch a runaway re-render loop in the act, instead of staring at the React DevTools Profiler flame chart trying to reverse-engineer a motive.
Getting the state updater’s story before it changes it. Freeze the scene inside the updater function itself:
const [cart, setCart] = useState([]);
function addItem(item) {
// breakpoint here
setCart((prev) => [...prev, item]); // inspect `prev` and `item` live
}
You see the exact previous state and the exact incoming item — not a console.log you now have to scroll back through a noisy terminal to find.
When you can’t plant the trap ahead of time. Sometimes you can’t set a breakpoint in advance — inside a dynamically evaluated callback, or a .map() written inline. Drop debugger; directly into the source instead:
items.map((item) => {
if (item.total < 0) debugger; // freezes only when this condition is true
return <Row key={item.id} {...item} />;
});
It behaves exactly like a conditional breakpoint, except it lives in the file — pull it before you commit, or just use VS Code’s native conditional breakpoint instead, which never touches your source in the first place.
Two investigators, two different jobs. React DevTools’ Components tab is for structure — the props/state tree, which component owns what. VS Code’s debugger is for behavior — why this specific function ran, with these specific values, right now. Use DevTools to find which component is a suspect, then drop into that component’s code with the debugger to find out why it did what it did. Neither tool replaces the other.
Chapter 7 — When the case crosses jurisdictions
Working two scenes at once (multi-target debugging). The Next.js compound config above is one instance of a general pattern — client+server, or any monorepo split, works the same way. You get one Call Stack and Variables view per active target, switchable right from the Call Stack panel.
The case moved to another building (remote and container debugging). If your app runs in Docker or on a remote box, expose the inspector port (--inspect=0.0.0.0:9229) and use an attach config with "address" pointing at the remote host — or install the Dev Containers extension so VS Code debugs inside the container as if it were standing right there with you.
A mistranslated report is worse than no report (source maps). If breakpoints show up as hollow, unbound circles instead of solid red, your bundler’s source maps don’t match what VS Code expects — you’re reading a translated version of the scene that doesn’t line up with the original. Check, in order:
- Your bundler actually emits source maps in dev (
devtool: 'eval-source-map'in webpack; Vite does this by default, no config needed). webRootin yourchromeconfig points at the real source root.sourceMapPathOverridesmatches your bundler’s URL scheme — webpack’swebpack:///prefix is the most common mismatch, and the most common reason breakpoints silently refuse to bind.
A witness can reconstruct a story that didn’t quite happen (async stack traces). A Promise/async chain’s call stack is sometimes a reconstructed trace, not the literal, original call path. If a frame looks off, use Step Into once to confirm you’re actually standing where you think you are before you trust it as fact.
Field manual — quick reference
| Action | Shortcut |
|---|---|
| Start/Continue | F5 |
| Step Over | F10 |
| Step Into | F11 |
| Step Out | Shift+F11 |
| Restart | Ctrl+Shift+F5 |
| Stop | Shift+F5 |
| Toggle breakpoint | F9 |
| Open Debug Console | Ctrl+Shift+Y |
| Toggle Auto Attach | Command Palette → “Debug: Toggle Auto Attach” |
The one thing that actually makes Reacher good at this
It was never the size of the guy. It’s that he’s right about what’s behind the door before he opens it, almost every single time, because he refused to open it on a guess. Debugging rewards the exact same discipline. Predict before you step. Predict before you expand a variable. Predict before you read the call stack. When reality agrees, you’ve confirmed the model in your head is correct. When it doesn’t, you’ve just found the bug — directly, from live evidence, instead of inferring it after the fact from print statements you had to guess the right ones to write.
Set up one real launch.json for something you’re working on today. Put a breakpoint in the one function you’re least sure about. State your theory before you freeze the scene. Watch how fast the actual story comes out.
Was this article helpful?
Your feedback helps us create better content.
Discussion.
Handpicked for you.
"Continuity is the secret to mastery. If you enjoyed our deep dive into Debug Like Reacher: The Complete VS Code Case File, we've selected these related insights based on your unique interests."