Debugging becomes much easier when you can pause a program, inspect its state, and move through it one statement at a time. This guide explains how to use a debugger to find the cause of incorrect behavior instead of guessing from the code alone.
What stepping through code means
When a program runs normally, the computer executes instructions quickly and you see only the final result. A debugger lets you interrupt that execution at a chosen line and examine what is happening internally.
Stepping through code usually involves four actions:
- Setting a breakpoint where execution should pause.
- Starting the program in debugging mode.
- Inspecting variables, function arguments, and the call stack.
- Moving through the program with commands such as Step Over, Step Into, and Step Out.
This process helps answer questions such as:
- Did this function run at all?
- What value did this variable have at a particular moment?
- Which branch of an
ifstatement was selected? - Why did a loop run too many or too few times?
- Which function called the code that failed?
Most modern editors include a graphical debugger. Visual Studio Code, Visual Studio, IntelliJ IDEA, PyCharm, Android Studio, browser developer tools, and many language-specific IDEs use similar concepts even though their buttons and configuration files differ.
Prepare your program for debugging
Before starting, make sure you can reproduce the problem. A debugger is most useful when you can repeatedly reach the same incorrect result.
- Run the program normally and record what happens.
- Identify the smallest input or action that triggers the problem.
- Locate the function, event handler, or route most likely connected to it.
- Confirm that you are running the current version of the source code.
- Make sure the required debugger extension, SDK, or runtime is installed.
Many tools support both a normal run configuration and a debug configuration. The debug configuration may need information such as the program entry point, command-line arguments, environment variables, working directory, or browser URL.
For compiled languages, use a build that includes debug symbols. Without them, the debugger may show machine instructions instead of meaningful source lines, or it may be unable to display local variables accurately. For JavaScript and TypeScript, source maps allow the debugger to connect generated code with the original source.
Avoid changing several things before the first debugging session. If you edit the program repeatedly without checking what it is doing, you can lose the original behavior and make the investigation harder.
Set a breakpoint
A breakpoint tells the debugger to pause before executing a particular line. In most editors, click in the narrow margin next to a source-code line. A colored marker appears when the breakpoint is active.
Choose a line where the relevant state is available. For example, if a function calculates a shipping total, place a breakpoint at the start of that function or immediately before the final calculation:
function calculateTotal(items, shippingRate) {
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
const shipping = subtotal * shippingRate;
return subtotal + shipping;
}
A breakpoint on const shipping = subtotal * shippingRate; allows you to inspect both subtotal and shippingRate before the multiplication occurs.
You can usually manage breakpoints in a dedicated panel. Common options include:
- Enable or disable a breakpoint without deleting it.
- Add a condition so it pauses only when an expression is true.
- Add a hit count so it pauses after a line executes a certain number of times.
- Add a logpoint that records a value without stopping execution.
- Remove all breakpoints when the session becomes cluttered.
Conditional breakpoints are particularly useful in loops. Instead of pausing on every iteration, you might pause only when index === 50 or when user.id matches a problematic record.
Start a debugging session
Start the program using your editor’s Debug or Run and Debug command. You may also launch a browser’s developer tools, attach to an already running process, or use a command-line debugger.
When execution reaches an enabled breakpoint, the program pauses before the marked statement runs. The debugger normally highlights the current line and opens panels containing variables, the call stack, breakpoints, and a debug console.
At this point, do not immediately resume. First inspect the values visible in the current scope. Look at function parameters, local variables, and relevant object properties. If a value is already wrong when it enters the function, the real defect is probably earlier in the call chain.
If the breakpoint is never reached, check the following:
- The code path may not be executed.
- The breakpoint may be in a different file than the running build.
- The program may be using a cached or older version.
- Source maps or debug symbols may be missing.
- The debugger may be attached to the wrong process.
- A condition on the breakpoint may be false.
A breakpoint that appears hollow, gray, or inactive often indicates that the debugger could not bind it to executable code.
Use the stepping controls
The exact labels vary by tool, but these commands have consistent meanings:
| Command | What it does | When to use it |
|---|---|---|
| Continue | Runs until the next breakpoint or exception | Skip code you already understand |
| Step Over | Executes the current line without entering called functions | Check the result of a function call quickly |
| Step Into | Enters the function called by the current line | Investigate that function’s internal behavior |
| Step Out | Finishes the current function and returns to its caller | Leave a function after finding enough information |
| Restart | Starts the debugging session again | Reproduce the same path from a clean state |
| Pause | Interrupts running code | Inspect a program stuck in a loop or long operation |
Suppose the current line is:
const total = calculateTotal(items, rate);
Choose Step Into if you want to inspect calculateTotal. Choose Step Over if you trust that function and only need to see the value assigned to total. Step Over still executes the function; it simply does not open its internal lines.
Use Step Out when you have entered a library or helper function and it is not relevant to the problem. It runs the remaining statements in that function, so do not use it if those statements may change the evidence you need.
After each step, inspect what changed. Compare the values before and after an assignment, function call, loop iteration, or conditional branch. A useful debugging session is not just a sequence of button clicks; it is a series of focused questions about how program state changes.
Inspect variables and expressions
The Variables panel normally separates values into scopes such as local, global, and closure or module scope. Expand arrays and objects to inspect their properties. Hover over an identifier in the editor to see its current value.
Use the debug console or watch panel to evaluate expressions while execution is paused. Useful expressions might include:
items.length
items[0]
subtotal + shipping
user && user.profile && user.profile.email
Be careful when evaluating expressions that have side effects. Calling a function, modifying an object, advancing an iterator, or changing a global variable can alter the program while you are investigating it. Prefer read-only expressions first.
Watch expressions are useful when a value matters throughout a long session. Add variables such as cart.total, request.status, or retryCount so you do not need to search for them after every step.
For complex objects, check whether the displayed value is a live view or a snapshot. Some tools update an expanded object after later code changes it, which can make the earlier state appear different. When timing matters, inspect primitive values or copy relevant properties into the console.
Read the call stack
The call stack shows the chain of functions that led to the current line. The top frame is usually the function currently paused. Lower frames are its callers.
For example, a stack might look like this:
validateOrder
submitCheckout
handleSubmit
onClick
Selecting another frame lets you inspect that caller’s local variables and source line. This is valuable when a function receives an unexpected argument. Move down the stack and ask where the value was created, transformed, or overwritten.
A stack trace also helps distinguish application code from framework or library code. If the top frames belong to a library, use the stack to find the first frame belonging to your own project. That is often the most useful place to investigate.
Async programs can make stack traces more complicated. Promises, callbacks, timers, and event handlers may separate the original action from the later failure. Enable async stack traces when your tool supports them, and inspect the request, callback, or event that created the current operation.
Debug conditions, loops, and exceptions
For an if statement, step to the line and inspect the condition’s values. Do not assume the branch is wrong until you know which comparison produced the result. Pay attention to type differences, missing properties, empty strings, and unexpected null or undefined values.
For loops, check all of these values:
- The initial counter or iterator.
- The loop condition before each iteration.
- The value being processed.
- Any code that changes the counter or collection.
- The value after the final iteration.
A conditional breakpoint is often better than manually pressing Step Over dozens of times. For example, pause when record.status === 'failed' or when index >= records.length.
Most debuggers can pause when an exception is thrown, even if the application later catches it. Enable break-on-exception when an error seems to disappear inside a try/catch block. Inspect the exception message, stack, inputs, and state at the exact point where it was raised.
If stopping on every framework or third-party exception creates too much noise, configure the debugger to pause only on uncaught exceptions or restrict breakpoints to your project files.
A practical debugging workflow
Use this repeatable process for a bug:
- Reproduce the problem with the smallest reliable input.
- Form a specific hypothesis, such as “the discount is applied twice.”
- Place a breakpoint before the suspected operation.
- Start debugging and inspect the incoming values.
- Step over simple lines and step into only relevant functions.
- Follow the call stack when a value is unexpected.
- Compare the actual state with the state your hypothesis predicts.
- Move the breakpoint earlier if the value was already wrong.
- Move it later if the value becomes wrong after a particular operation.
- Make one focused code change and reproduce the issue again.
This approach prevents random stepping. Each breakpoint should help answer a question. If it does not, remove it or move it.
Troubleshooting common debugger problems
If the debugger skips your breakpoint, verify that the source file is the one loaded by the running program. Check build output, source-map paths, package versions, and the current working directory. Restarting the debug session can also clear stale generated files.
If variables show as unavailable, the code may have been optimized, the current scope may no longer exist, or the debugger may be paused on a line that executes before the variable is initialized. Debug builds and reduced optimization usually provide clearer values.
If stepping behaves strangely, asynchronous execution may be involved. A Step Over operation can appear to jump away because the next part runs in a callback, timer, or promise continuation. Set a breakpoint inside that callback and continue.
If the application freezes, use Pause to inspect the current stack. An infinite loop often shows the same frames repeatedly. Check the loop condition and whether the value controlling it can actually change.
If the debugger attaches to the wrong process, stop all duplicate servers or applications and confirm the selected process, port, runtime, or launch configuration. This is common when a development server automatically starts child processes.
Debugger limitations and good habits
A debugger shows the state at selected moments; it does not automatically prove why the program is wrong. Pausing can change timing, especially in multithreaded, real-time, networked, or race-condition bugs. Some problems disappear when the program runs more slowly.
Do not use the debugger as a replacement for logging, tests, code review, or clear error handling. Logs are better for behavior that occurs in production, while automated tests are better for preventing a fixed bug from returning. For sensitive applications, avoid exposing passwords, tokens, personal data, or private request payloads in debugger panels or screenshots.
Keep sessions focused. Remove temporary breakpoints, restore altered values, and record the discovered cause. Once you understand the failure, write a small regression test or reproduce the scenario through the normal workflow so the fix can be verified without relying on a paused process.
With practice, stepping through code becomes a fast way to turn a vague symptom into a precise sequence: the input entered, the state changed, the branch ran, and the value became incorrect. That sequence gives you the evidence needed to make a targeted fix.