On this page
Console logging works until a request passes through several asynchronous functions and the value you need disappears between messages. The VS Code debugger lets you stop at the decision point, inspect the current scope, and follow the actual execution path without adding permanent logging noise.
Prepare a small, repeatable failing request
Capture one input that reliably demonstrates the defect. If the failure involves JSON, inspect a sanitized payload with the JSON formatter before replaying it locally. Check missing fields, strings that should be numbers, and nested arrays. Do not paste production credentials or customer records into troubleshooting tools.
Write down the expected result and observed result. A useful example is “quantity zero should be rejected, but the endpoint returns a total.” This gives you a specific branch to inspect rather than an invitation to step through every line of the server.
Use a local development process with test data. Pausing a Node.js process can hold requests open and trigger client timeouts. Other workers or subprocesses may continue, so a breakpoint does not necessarily freeze the entire application. The VS Code Node.js debugging guide explains the available launch and attachment modes.
Launch the program directly before adding wrappers
For a small service, start with a launch configuration that points directly at the JavaScript entry file. This removes uncertainty about package-manager wrappers, development servers, and which child process actually runs the application.
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug local API",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/src/server.js",
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**"],
"env": {
"NODE_ENV": "development"
}
}
]
}Save this as .vscode/launch.json, adapting the entry path to the repository. The working directory matters when code resolves relative file paths. The integrated terminal is useful for programs that expect terminal input; the Debug Console serves a different purpose and is not a general replacement for standard input.
Set a breakpoint in the request handler, start the configuration, and send your failing request. Confirm the breakpoint stops before adding complicated watches. If the application exits immediately, read the terminal error first: the debugger cannot compensate for missing dependencies or a wrong entry point.
Keep shared launch files free of credentials and machine-specific absolute paths. A configuration should explain how to start the program, not silently encode one developer's laptop layout.
Attach when another tool starts the process
Sometimes a framework command or existing terminal owns startup. Start Node with its inspector enabled, then attach VS Code to the inspector port. Use --inspect-brk when you need to stop before application startup code runs; plain --inspect normally lets execution continue immediately.
node --inspect-brk=127.0.0.1:9229 src/server.js{
"name": "Attach local Node",
"type": "node",
"request": "attach",
"address": "127.0.0.1",
"port": 9229,
"skipFiles": ["<node_internals>/**"]
}Add the second object to the configurations array. Keep the inspector on loopback only for normal local work. The Node.js debugging guide explains why exposing an inspector endpoint is dangerous: debugger access can enable arbitrary code execution in the process.
If a watcher restarts the server, verify whether the attach configuration reconnects to the new process. A stale attachment can look like ignored breakpoints. For containers or remote development, prefer the editor's supported remote workflow or a deliberate tunnel rather than binding the inspector publicly.
You can also try a JavaScript Debug Terminal for convenient automatic attachment. Regardless of the method, confirm the selected process is the one receiving your request, especially when a command starts multiple Node processes.
Inspect state without changing the bug accidentally
At a breakpoint, start with local variables and the call stack. Ask where the unexpected value first becomes wrong. Step over executes the current operation without following every called function; step into follows a call, and step out returns from the current function. Use these deliberately instead of repeatedly pressing the same key.
Add a conditional breakpoint such as request.body.quantity === 0 when the handler receives many requests. The expression must use names available at that location. A condition that throws or references nonexistent variables can confuse the investigation rather than narrow it.
Use logpoints for observations that do not require pausing. They are useful in loops or frequently called functions, although expensive expressions still add overhead. Avoid printing tokens, full authorization headers, or entire customer payloads just because the output is temporary.
The Debug Console evaluates expressions in the selected stack frame. Prefer inspection over mutation: calling a method may change state, send a request, or advance an iterator. A getter can also have side effects. If you deliberately change a variable to test a hypothesis, restart the scenario afterward before drawing conclusions.
Enable exception breakpoints when a failure is being swallowed. Start with uncaught exceptions, then consider caught exceptions if necessary. Pausing on every caught exception in a dependency-heavy application can be overwhelming, so narrow the reproduction first.
Resolve source maps and verify the fix normally
A hollow breakpoint often means the debugger cannot map your source file to executable code. For TypeScript compiled to JavaScript, enable source maps in the compiler and ensure the launch configuration's output-file patterns match the emitted JavaScript. Do not point at an old build directory while editing newer source files.
Read the VS Code TypeScript debugging documentation for the mapping model. Bundlers and runtime transpilers need their own compatible configuration; copying a plain compiler setup into every framework rarely works. Remove stale output and rebuild using the project's normal command when mappings no longer line up.
Check the loaded script path and active process before changing compiler settings. Duplicate source copies, symlinked packages, and monorepo working directories can all send you to the wrong file. Source-map accuracy is a prerequisite for trusting line-level observations.
Once you understand the failure, write a focused regression test and run it outside the debugger. Timing-sensitive defects may disappear while paused. Confirm ordinary startup, then remove temporary watches or diagnostic settings that could expose sensitive data. Keep useful team configurations documented alongside broader development practices and the dev-tools collection.
Key takeaways
- Reproduce one concrete failure before stepping through the application.
- Launch directly when possible; attach deliberately when another process owns startup.
- Keep inspector access local and verify the actual process receiving requests.
- Use conditions and logpoints to reduce noise without mutating state.
- Trust source mappings only after checking emitted files and loaded paths.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor