Blog

How to Use a Debugger and IDE Tools Effectively

A debugger lets you pause a running program, read the actual values of its variables, and move forward one line at a time, so you stop guessing what the code does. The core skill is small: set a breakpoint just before the suspicious code, run in debug mode, step through while watching variables, and compare each value with what you expected. The first value that differs from your expectation is where the bug starts.

This guide covers that workflow with a worked example, the features that save the most time (conditional breakpoints, logpoints, the call stack, “run to cursor”), the shortcuts in VS Code and the JetBrains IDEs, and when print statements or tests are the better tool.

The basic loop

  1. Reproduce the bug with a specific input you can run again. If you cannot repeat it, you cannot tell whether you fixed it.
  2. Form a guess about where the program first goes wrong. It need not be right; it tells you where to put the first breakpoint.
  3. Set a breakpoint on that line (click in the gutter next to the line number).
  4. Start in debug mode. The program runs until it reaches the breakpoint and pauses.
  5. Inspect and step. Read the variables, step one line at a time, and ask at each line whether the values are what you predicted.
  6. Fix, rerun, and keep the input as a test so the bug cannot come back unnoticed.

The controls you actually need

Action What it does VS Code IntelliJ / PyCharm
Continue / Resume Run until the next breakpoint F5 F9
Step over Run the current line; do not enter called functions F10 F8
Step into Enter the function called on this line F11 F7
Step out Finish the current function and return to the caller Shift+F11 Shift+F8
Run to cursor Run until the line where the cursor is right-click, “Run to Cursor” Alt+F9
Toggle breakpoint Add or remove a breakpoint on this line F9 Ctrl+F8 (Cmd+F8 on macOS)

These are the default key maps on Windows and Linux; macOS and custom key maps differ, so check your IDE’s keymap settings if they do nothing. As a rule, step over code you trust (library calls, helpers already tested) and step into only the function you suspect.

A worked example

This function should return the average of the positive numbers in a list:

def average_of_positives(nums):
    total = 0
    count = 0
    for n in nums:
        if n > 0:
            total += n
        count += 1
    return total / count

print(average_of_positives([4, -2, 6, -8, 2]))

The positives are 4, 6 and 2, so the answer should be 4.0. It prints 2.4. Which line is wrong? Do not read the code again; use the debugger.

Put a breakpoint on the count += 1 line and start debugging. Add total and count to the Watch panel (or look at the Variables panel). Each time you resume, the program pauses on that line again. The values when the program reaches that line, as (n, total, count), are:

(4, 4, 0)
(-2, 4, 1)

On the second pause n is -2, which the if skipped, yet count is already 1 and will become 2. The counter is advancing for numbers that were not added to the total. The count += 1 line belongs inside the if, next to the update of total. The final division was 12 / 5 = 2.4 instead of 12 / 3. With the lines indented correctly the function returns 4.0.

A tidier version removes the shared counters altogether:

def average_of_positives(nums):
    pos = [n for n in nums if n > 0]
    return sum(pos) / len(pos)

This prints 4.0 for the example input. The debugger also finds the next bug: with [-1] the list pos is empty and the function raises ZeroDivisionError. Decide what an empty result should mean (return 0, None, or raise a clearer error) and write a test for it.

Features that save time

Conditional breakpoints

If the bug shows up on the 5,000th iteration, you do not want to press Resume 4,999 times. Right-click the breakpoint and add a condition such as n < 0 or i == 4999. Execution pauses only when the condition is true. In the example above, the condition n < 0 skips the positive numbers and stops on -2 and then -8. A hit count (“break on the 100th time”) does the same thing without an expression. Conditions run on every pass through the line, so a heavy expression in a hot loop slows the program down.

Logpoints

A logpoint prints a message when execution passes the line but does not pause, like a print statement that you never have to commit or delete. VS Code and the JetBrains IDEs both support them (in IntelliJ, a breakpoint set to log an expression instead of suspending). They are the right choice for timing-sensitive code, where stopping would change the behavior.

The call stack

The Call Stack panel lists the chain of function calls that led to the current line. Click a frame and the Variables panel shows that function’s locals. When a function receives a bad argument, the stack lets you go up one level to see where the bad value came from. This is faster than searching the code. For an exception, enable “break on uncaught exceptions” (or on all exceptions) and the debugger stops at the line that raised it with the whole state intact.

Evaluate and modify

The Debug Console (VS Code) or “Evaluate Expression” (JetBrains) runs code in the paused frame: check len(nums), call a helper, or test a candidate fix. Most debuggers also let you edit a variable’s value in the panel to try a different path without restarting. Treat this as an experiment; the real fix still goes in the source.

Run to cursor

Instead of stepping through twenty lines, click the line you care about and use Run to Cursor. It is a one-shot temporary breakpoint.

Data breakpoints (watchpoints)

Some debuggers (GDB, LLDB, Visual Studio, JetBrains for several languages) can pause when a variable or memory location changes. This answers “who changed this value?” when the culprit is far away. Support depends on the language and debugger.

Debugging in the terminal and browser

You do not need an IDE for the same workflow. In Python, breakpoint() drops into pdb at that line (or run python3 -m pdb script.py). The commands are n (next, step over), s (step into), c (continue), p expr (print), bt (stack), b 7 (breakpoint at line 7) and q. In JavaScript, put the statement debugger; in your code, open the browser’s DevTools, and execution pauses there. The Sources panel offers the same breakpoints, watches and stack. For Node, start with node --inspect-brk file.js and attach VS Code or Chrome DevTools.

Debugger, print statements or tests?

  • Debugger: best when you need to see the full state at one moment, follow unfamiliar code, or look at many variables at once.
  • Print or logging: better when the bug depends on timing (concurrency, network), when you need a history of many events rather than one moment, or when you only have access to a server’s logs. Structured logs with levels are the standard for production.
  • Unit tests: best for pinning down a bug once it is understood. A failing test is also a repeatable way to start the debugger: debug the test.
  • Bisecting: if something used to work, git bisect finds the commit that broke it by binary search.

Multithreaded and race-condition bugs are hard to catch under a debugger because pausing changes the timing; use logging, thread dumps, and tests designed to provoke the race. For memory leaks, use a profiler or heap snapshot instead of stepping.

Other IDE features worth learning

  • Go to definition and find usages (F12 and Shift+F12 in VS Code; Ctrl+B and Alt+F7 in IntelliJ) let you navigate code by meaning instead of by search.
  • Rename symbol (F2 in VS Code, Shift+F6 in JetBrains) changes every reference safely, which find-and-replace cannot promise.
  • Quick fixes (Ctrl+. in VS Code, Alt+Enter in JetBrains) apply the suggested fix for an error or warning.
  • Extract function or variable restructures code with the IDE checking the result.
  • Linters and type checkers (Pylance or mypy, ESLint, TypeScript) catch whole classes of bugs before you run anything.
  • Source control panel shows diffs line by line; reading your own diff before committing catches mistakes cheaply.

Learn the shortcut for each of the five or six actions you use daily rather than memorizing a list. Both IDEs show shortcuts in their command palettes (Ctrl+Shift+P in VS Code, Shift twice in JetBrains).

Common mistakes

  1. Stepping through everything. Put the breakpoint near the failure and work backwards, or use a conditional breakpoint.
  2. Not predicting values. Decide what you expect before you look. Without a prediction you cannot notice the surprise.
  3. Changing several things at once. Change one thing, rerun, and observe.
  4. Fixing the symptom. Here, a patch such as count -= 1 after a negative number would “work” but leave the logic wrong.
  5. Debugging optimized or minified code. Use debug builds or source maps, otherwise lines and variables will not match your source.

Our post on why debugging takes longer than writing code covers the thinking side, and debugging code during a coding interview covers what to do when you have no debugger, only a whiteboard or a shared editor. To practice reading code and finding what is wrong with it, AlgoCademy’s Python course has buggy-code exercises, such as Indentation & Execution Flow: Buggy Code, which is close to the bug in the example above, and you can practice there with step-by-step guidance.

A lesson in AlgoCademy: the step to write, a picture of what it does, the line written in the editor, and the check: Step 2 passed

Before you go

Start thinking like a software engineer.

Learn to code and solve interview problems, one small, checked step at a time.

Free, no card needed. Or look around first

By continuing you agree to the Terms of Service and Privacy Policy.