Developer Productivity

Breakpoints That Log and Conditions That Don't Pause

Replace scattergun console.log statements with conditional breakpoints and logpoints in VS Code and DevTools. Set targeted pauses and inline logs without modifying code.

Mohammed Saqib9 min read
Man focused on coding at his workstation in a modern office.
Photo by cottonbro studio on Pexels · Pexels License

Every codebase has that one file where a console.log('here') has been sitting for months because nobody is sure whether deleting it will break someone else's debugging session. The standard loop — insert a log, save, rebuild, reproduce, delete the log — costs a context switch every single time. Conditional breakpoints and logpoints collapse that loop into a right-click, with no source edit required.

What conditional breakpoints and logpoints actually are

A conditional breakpoint is a breakpoint with an expression attached. When execution reaches the line, the debugger evaluates the expression and only pauses if it evaluates to true. Without the condition, you'd be stopping on every iteration and manually checking the same guard over and over; with it, the runtime skips past every hit that doesn't match.

A logpoint is the same interception mechanism, but instead of pausing, the debugger formats a message, prints it, and continues. No source file is touched, so there's nothing to revert and nothing to accidentally commit into a PR. Some debuggers call them tracepoints; the idea is identical.

Both are implemented by the debugger rather than the runtime, which is why they work across VS Code, Chrome DevTools, Firefox DevTools, and most IDE debuggers. The syntax differs slightly — VS Code and Chrome both use {expression} interpolation inside log messages — but the model is the same everywhere. This is debugging state without pausing: the values land in front of you while execution keeps flowing.

Setting up conditional breakpoints in VS Code and Chrome DevTools

In VS Code, right-click the gutter next to a line and choose Conditional Breakpoint. Type an expression like x > 5 and the breakpoint gets a small equals icon. The VS Code debugger conditional expression syntax also supports the built-in hitCount variable, so hitCount % 3 == 0 stops on every third visit to that line.

In Chrome DevTools, right-click a line number and choose Add conditional breakpoint. The expression syntax is the same, and because it's plain JavaScript, Math.random() < 0.1 works as a sampling mechanism. Both debuggers evaluate the expression in the scope where the line executes, so local variables, closure variables, and this are all visible. The full expression grammar is documented in the VS Code debugging documentation and Chrome's breakpoint docs.

Consider this function:

type Order = { id: string; total: number };
 
function processOrders(orders: Order[]) {
  let total = 0;
  for (let i = 0; i < orders.length; i++) {
    total += orders[i].total;
  }
  return total;
}

Set a conditional breakpoint on the total += line with orders[i].total > 1000 and you stop only when a single order crosses the threshold. A plain breakpoint would stop on every iteration, and you'd be pressing continue forty times before anything useful appeared. The condition can use logical operators, property access, and function calls — keep the calls read-only, for reasons covered later.

Logpoints: the killers of console.log archaeology

The logpoint is the tool that finally retires the "I'll just add a console.log" reflex. In VS Code, right-click a line, choose Logpoint, and type a message like Order {id} processed. Every {expression} in the message is evaluated and interpolated at hit time. Chrome DevTools logpoints work the same way: right-click a line, choose Add logpoint, and use the same brace syntax. Output goes to the Debug Console in VS Code and the Console panel in Chrome — not to the application's own console object, which matters in Node because process output and debugger output are separate channels.

The win is structural: no editing source files, no risk of committing debug prints, no rebuild or hot-reload wait. If a logpoint turns out to be noisy, you delete it with one click. If a codebase relies on a pre-commit hook to flag stray console.log statements — the kind of hook covered in Pre-Commit Hooks Developers Actually Keep Enabled — a logpoint makes that hook unnecessary for new debugging, because the log never lands in the file to begin with.

This is the core of the "debug without console.log" workflow. You get the value, you see it in context, and the source tree stays clean. The act of "inserting a log" stops being a code edit and starts being a debugger gesture.

When to use which: trade-offs and performance

The real question is whether pausing is acceptable.

Logpoints are non-blocking. The debugger hits the line, formats the message, prints it, and continues. On high-frequency paths — render loops, request handlers, message callbacks — the only cost is the debugger hit itself, typically milliseconds per thousand hits. That's not free, but it's far cheaper than a pause-resume cycle.

Conditional breakpoints are for when you need to inspect state at a specific moment and the pause is fine: hit-once scenarios, rare conditions, or call-stack inspection. The condition is evaluated on every hit, so a slow expression on a hot path adds up. A hit count breakpoint stops after a fixed number of visits and combines well with a condition to bound the cost — the condition still runs every time, but the pause only fires when both are satisfied.

The conditional breakpoints vs logpoints decision, then, is really about whether you need the call stack. If a value looks wrong and you need to know why, pause. If you need to confirm a value is correct across a thousand iterations, log it.

Logpoint Conditional breakpoint Hit count breakpoint
Pauses execution No Yes Yes
Condition support Always logs when hit Expression must be true Fires after N hits
Output Debug Console / Console panel Debugger stops, inspect scope Debugger stops
Cost per hit Low, non-blocking Higher: evaluates expression, then pauses Higher: pauses every N hits
Best for High-frequency logging Rare condition inspection Loop limits, N-th call

Breakpoint side effects are the classic footgun in all three. A condition like array.pop() changes program state while you're debugging, which means the bug you're chasing mutates under you. Same with count++ inside a logpoint message. The expression should be read-only, full stop.

Failure modes and gotchas

Expressions that throw are silently ignored. If a condition references undefined.a, the debugger can't evaluate it, and in most debuggers the breakpoint simply doesn't fire — no error in the console, no pause, just a line that runs normally. The same applies to logpoints: a throwing interpolation drops the message.

Misspelled variable names behave worse because they fail closed. A typo like totaal > 5 evaluates to undefined, which is falsy, so the condition never fires and you wait for a bug that never comes. In a logpoint, the same typo prints undefined in the middle of an otherwise plausible message, which is at least visible.

Minified or transpiled code breaks name matching. Source maps must be loaded for your variable names to resolve; without them, a condition like order.total > 1000 references minified names like e or n, and the expression silently never matches. Verify the source map is active before trusting a conditional breakpoint in a bundled app.

Async code adds another layer. A conditional breakpoint on a line inside a callback can fire on ticks you didn't expect if the condition depends on state that changes asynchronously. The condition is evaluated synchronously at the pause point, so if a promise resolves between the check and the hit, you can get surprising pauses or misses.

Migrating from console.log workflows

Stop inserting and removing console.log lines. The next time you reach for one, set a logpoint instead. If you want to see every invocation of a function with its arguments, set a logpoint on the function's first line and interpolate {arguments} — the debugger exposes the arguments object directly, so no manual spread or join needed.

For Node.js services, attach the debugger and keep the process running. The official Node.js debugging guide walks through the attach flow; with VS Code it amounts to a launch configuration like this:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "attach",
      "name": "Attach to Node",
      "port": 9229,
      "restart": true
    }
  ]
}

Start the process with node --inspect=127.0.0.1:9229 server.js, attach, and logpoints are live immediately. Add or remove them without restarting — no rebuild, no redeploy. That immediacy is what makes the "debug without console.log" habit stick: the debugger is already attached, so setting a logpoint is faster than typing a console.log and redeploying.

If you maintain a codebase that already has thousands of console.log calls to clean up, a codemod approach like the one in Large-Scale Refactors with jscodeshift and ts-morph can strip them in one pass. The logpoint habit prevents the next generation from accumulating.

VS Code's Stop on Entry breakpoint complements this. Set it on the entry function, let the debugger break on the first invocation, inspect the initial state, then add your conditional breakpoint or logpoint and remove the entry breakpoint. You get the full lifecycle — first state, targeted pause, then clean exit — without ever editing a file.

Advanced patterns

To snapshot an object without pausing, use a logpoint like {JSON.stringify(account, null, 2)} for a formatted dump. Watch for circular references: JSON.stringify throws on them, and the debugger will silently drop the output. If you need to inspect something with cycles, a conditional breakpoint that pauses and lets you walk the scope graph is the more reliable option.

Sampling works well for intermittent failures. A condition of Math.random() < 0.05 catches roughly 5% of calls along a hot path, which is enough to observe a rare state without paying the cost on every invocation. Combine it with a hit count if you need to start sampling after a warmup period.

One thing that is not supported: overriding console.log with a conditional breakpoint that never stops. A logpoint prints to the debugger console, and you can't inject one into the middle of an existing console.log call. VS Code's trace option in launch.json writes debug adapter protocol traffic to a file, which helps diagnose the debugger itself, but it is not a sink for logpoint messages. If you need a persistent copy of logpoint output, keep the Debug Console open and copy from there.

Key takeaways

  • Conditional breakpoints pause only when an expression is true; logpoints print and keep going.
  • Both exist in VS Code and Chrome DevTools with the same {expression} message syntax.
  • Use logpoints for hot paths and confirmation logging; use conditional breakpoints when you need the call stack.
  • Keep conditions read-only — a mutating expression changes the program you're trying to debug.
  • Attach the Node debugger and leave source files alone, and the console.log archaeology stops being a habit.

Frequently asked questions

Can I use logpoints in production?
Logpoints require an attached debugger. In production, no debugger is running, so they have no effect. Production observability needs proper structured logging with log levels and transports.
Do conditional breakpoints affect performance?
Yes. Every time the line is hit, the debugger evaluates the condition expression. For hot functions or tight loops, this overhead can be significant. Use hit count or logpoints instead to avoid evaluating conditions on every invocation.
How do I log a local variable without pausing execution?
Add a logpoint (right-click the line gutter) and enter a message like `User clicked button at {timestamp}`. The variable name in braces will be replaced with its value. Execution continues uninterrupted.
#conditional-breakpoints#logpoints#debugging#vscode#ide-tools
Share

Keep reading