📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is a breakpoint, and how do you step through .NET code with Step Into, Step Over and Step Out?

The basics of interactive debugging in Visual Studio, VS Code and Rider: setting breakpoints, stepping commands, Run to Cursor and Set Next Statement, and why Debug builds and symbols matter.

Beginner Theory Beginner breakpoints debugging developer-tooling stepping visual-studio

🎯 Key points

  • A breakpoint pauses the process at a line so you can inspect state
  • Set with F9 or a click in the editor gutter; start debugging with F5
  • Step Over (F10) runs the current line; Step Into (F11) enters a call; Step Out (Shift+F11) finishes the current method
  • Run to Cursor and Set Next Statement move execution without extra breakpoints
  • Debug builds and PDB symbols make breakpoints map reliably to source lines
  • A hollow breakpoint means it will not be hit, usually because symbols are not loaded or the code is not what is running
  • Breakpoints pause only under a debugger; never leave Debugger.Break() in shipped code

⏱ 30-second answer

A breakpoint tells the debugger to pause the process when execution reaches a line, so I can look at variables and the call stack. I set one with F9 or a click in the gutter and start with F5. Then I step: Step Over (F10) runs the current line, Step Into (F11) goes into the method being called, and Step Out (Shift+F11) runs to the end of the current method and returns to the caller. Run to Cursor and Set Next Statement move faster or re-run a line. It works best on a Debug build with symbols, because optimised code can skip or reorder lines.

🗣 Full answer (senior)

How I would answer this in an interview

"A breakpoint tells the debugger to pause the process when execution reaches a line, so I can inspect variables and the call stack. I set one with F9 or a click in the gutter and start debugging with F5; when it's hit, the runtime pauses all managed threads and hands control to the debugger.

Then I step. Step Over, F10, runs the current line including any calls; Step Into, F11, goes into the method being called; and Step Out, Shift+F11, finishes the current method and returns to the caller. Run to Cursor and Set Next Statement move faster or re-run a line, though Set Next Statement skips code, so state can end up inconsistent. If a breakpoint is hit too often, I make it conditional rather than pressing F5 again and again.

It works best on a Debug build with matching PDB symbols, because the PDB maps IL back to source lines. The mistake I see is debugging a Release build without realising it, where lines are merged or skipped and locals are optimised away. A hollow breakpoint means it won't be hit, usually because symbols aren't loaded or the running binary isn't the one I built, so I check the Modules window. And because pausing stops every thread, HTTP and database timeouts fire, which is why I never pause a production process and never leave Debugger.Break() in shipped code.

I use the debugger to test a hypothesis, not to wander, and I turn what I learn at the breakpoint into a failing unit test before I fix the bug."


What a breakpoint does

When you start a project under the debugger (F5), the debugger attaches to the process. A breakpoint marks a source line; when a thread reaches the code for that line, the runtime stops all threads and hands control to the debugger. You can then inspect locals, the call stack and other threads, change values, and continue.

The stepping commands

Command Visual Studio / VS Code key What it does
Continue F5 Run until the next breakpoint
Step Over F10 Execute the current line, including any calls, and stop on the next line
Step Into F11 If the line calls a method, stop at its first line
Step Out Shift+F11 Run the rest of the current method and stop in the caller
Run to Cursor Ctrl+F10 (Visual Studio) Run to the line the cursor is on
Set Next Statement Ctrl+Shift+F10 (Visual Studio) Move the instruction pointer to another line in the same method, without running the code in between

Rider has the same commands; the shortcuts depend on the keymap you choose (the Visual Studio keymap uses the keys above).

Working effectively

  1. Put the breakpoint just before the code you suspect, not at the start of Main.
  2. Step Over by default, and Step Into only the calls you care about. In Visual Studio, Step Into Specific (right-click) picks one call on a line that has several.
  3. Use Step Out as soon as you realise you are in the wrong method.
  4. Use Set Next Statement to re-run a line after changing a value, but remember it skips code: state can become inconsistent.
  5. If a plain breakpoint is hit too often, make it conditional (for example order.Number == "A-102") instead of pressing F5 repeatedly.

Why build configuration and PDBs matter

The compiler writes a PDB (program database) next to each DLL. It maps IL offsets to source file, line and column (sequence points) and records local variable names, and it carries a unique id that must match the DLL. Without a matching PDB the debugger cannot bind a source breakpoint or show the source line.

In a Debug build the compiler emits IL that matches your source closely and the JIT does not optimise it. In an optimised Release build, lines can be merged, reordered or inlined, so breakpoints may be skipped and some locals show as optimised away. Debug locally for logic bugs; switch to Release only when a bug appears only there.

When a breakpoint is hollow

A hollow circle ("The breakpoint will not currently be hit") means the debugger could not bind it. Hover over it for the reason, then check:

  1. Is the module loaded? Open Debug → Windows → Modules and find your DLL. If it is missing, that code has not run yet (normal for lazily loaded assemblies) or a different process is running it.
  2. Are symbols loaded? In Modules, Symbol Status should say Symbols loaded. If not, rebuild, or right-click → Load Symbols and point at the matching PDB.
  3. Is the running binary the one you built? A stale DLL in another folder, a NuGet package version instead of your project reference, or an app still running from a previous build all cause this. Clean and rebuild, and check the module path.
  4. Are you attached to the right process? For ASP.NET Core in IIS or a container, attach to the worker process that actually hosts the app.
  5. Is it a Release build? Optimised code can make some lines unbindable.

Production example

A GST total on an invoice service was consistently short by the first line item. Setting a breakpoint in the total method and stepping over the loop showed the index started at 1 instead of 0. Two minutes with the debugger replaced an hour of adding log lines.

🔍 What the interviewer is checking

The interviewer checks basic fluency with the debugger and whether you understand why it behaves differently in some builds.

Green flags

  • Knows the stepping commands and when to use each.
  • Mentions Debug builds, PDBs and why breakpoints become hollow.
  • Knows pausing affects timeouts and all threads.

Red flags

  • Debugs only with Console.WriteLine.
  • Cannot explain the difference between Step Over and Step Into.
  • Would attach and pause a production process casually.

📚 Learn in depth

1. What happens when you press F5

 F5 (Start Debugging)
   |
   v
 IDE builds the project (Debug configuration by default)
   |   compiler writes MyApp.dll + MyApp.pdb (portable PDB)
   v
 debugger launches the process (or attaches to an existing one)
   |   and connects through the runtime's debugging interface
   v
 runtime reports every module (DLL) it loads
   |   debugger looks for a matching PDB for each one
   v
 pending breakpoints in that module are BOUND
   (solid red circle)  or stay UNBOUND (hollow circle)

A breakpoint you set before the code has loaded is "pending". It binds as soon as the runtime loads the module and the debugger finds matching symbols.

2. How a breakpoint becomes a pause

 source line 42
     |  PDB sequence point
     v
 IL offset 0x1A in method OrderMath.Total
     |  JIT compiles the method to native code
     v
 native address 0x7FF6...
     |  debugger asks the runtime to patch it
     v
 breakpoint instruction (int 3 on x64, brk on Arm64)
     |  the CPU traps when it reaches it
     v
 runtime suspends ALL managed threads --> notifies debugger
     --> IDE highlights line 42 and shows locals

The PDB is the link between your source and the compiled code. It contains:

PDB content Used for
Sequence points (IL offset to file, line, column) Binding breakpoints and highlighting the current line
Local variable names and scopes The Locals and Watch windows
A unique id that must match the DLL Refusing a stale PDB from another build
Optional Source Link and embedded source Finding source for code you didn't build

3. The stepping commands in detail

Command Visual Studio / VS Code What the debugger actually does
Continue F5 Resumes all threads until a breakpoint or exception
Step Over F10 Runs the current line, including every call on it, and stops at the next sequence point in the same method
Step Into F11 If the line calls a method that has symbols and is "your code", stops at its first line. Otherwise behaves like Step Over
Step Out Shift+F11 Sets a temporary breakpoint at the return address in the caller and continues
Run to Cursor Ctrl+F10 (VS) A one-off temporary breakpoint at the cursor line
Set Next Statement Ctrl+Shift+F10 (VS), or drag the yellow arrow Moves the instruction pointer without running the code in between
Step Into Specific Right-click (VS) Chooses one call when a line has several, such as Save(Map(order))

Rider has the same commands. With its Visual Studio keymap the keys match the table.

Stepping is implemented with temporary breakpoints and single-stepping. That is why Step Over a call that waits on I/O looks like a hang: the call really is still running, and the debugger is waiting for it to return.

4. Stepping through async code

An await splits a method into a state machine. The code after the await may run on a different thread.

 async Task<decimal> TotalAsync()
 {
     var rates = await _rates.GetAsync();   <-- Step Over here
     return Sum(rates);                     <-- debugger stops here when
 }                                              the await completes,
                                                maybe on another thread

Modern debuggers are async-aware: Step Over on an await line waits for that specific continuation and stops on the next line of the same logical method, even if another thread runs it. Use the Tasks and Parallel Stacks windows in Visual Studio to see where async work is waiting.

5. Debug versus Release when debugging

Debug Release
Compiler optimisation Off; extra nop instructions keep one sequence point per line On; lines can be merged and dead code removed
JIT optimisation Off when a debugger launches the process (default) On; methods can be inlined and locals kept in registers
Breakpoints Bind to almost every line Some lines can't be bound or are skipped
Locals All visible Some show "optimised away"
Stepping Follows the source closely Can jump around

Debug locally for logic bugs. Switch to Release only when a bug only happens there, and expect a rougher stepping experience.

6. Why a breakpoint is hollow

Hover over the hollow circle for the reason, then work through this table:

Cause How to check Fix
Module not loaded yet Debug > Windows > Modules Normal for code that hasn't run. It binds when the module loads
Symbols not loaded Modules window, Symbol Status column Rebuild, or right-click > Load Symbols and point at the matching PDB
Stale binary Module path in the Modules window Clean and rebuild. Check you aren't running an old copy or a NuGet package instead of your project reference
Wrong process Debug > Attach to Process Attach to the process that hosts the app, such as the IIS worker or the container process
Release or optimised build Configuration dropdown Use Debug, or accept that some lines can't bind
Just My Code hides it Tools > Options > Debugging Turn off Just My Code to debug library code (see the follow-up question)

7. Debugging outside Visual Studio

Tool How
VS Code with C# Dev Kit F5 on a project, or a launch.json entry of type coreclr for a specific DLL
Rider Debug a run configuration, or attach to a local or remote process
Attach to a running app Visual Studio Ctrl+Alt+P, Rider Ctrl+Alt+F5, VS Code "Attach" configuration
Containers Visual Studio and VS Code container tools install the debugger in the container and attach

8. Performance and safety

  • While stopped, every managed thread is paused, so timers, HTTP timeouts, database command timeouts and health checks fire late or fail after you continue. Don't chase those failures as if they were the bug.
  • Never pause a shared or production process interactively. It stops serving everyone. Use logs, traces and dumps there.
  • Debugger.Break() and Debugger.Launch() left in code will try to start a debugger on a server or stop a test run. Remove them before committing, or guard them with #if DEBUG.

9. .NET 11 awareness

The stepping model is stable. Debugger improvements arrive through Visual Studio, Rider and the C# extension releases rather than the runtime, so check their release notes alongside .NET 11.

10. Try it yourself

  1. Create dotnet new console -n StepDemo and paste the code example.
  2. Set a breakpoint on decimal total = OrderMath.Total(orders); and press F5.
  3. Press F11 to step into Total, then F10 through the loop, and watch i in the Locals window.
  4. Press Shift+F11 to return to the caller and see the wrong total.
  5. Fix the loop, use Set Next Statement to move back to the call, and step again.
  6. Switch the configuration to Release, debug again, and compare how stepping and Locals behave.
  7. Open Debug > Windows > Modules and find StepDemo.dll and its symbol status.

💻 Code

A small program with a bug to find by stepping:

var orders = new List<Order>
{
    new("A-100", 120.50m),
    new("A-101", 89.95m),
    new("A-102", 230.00m),
};

decimal total = OrderMath.Total(orders);
Console.WriteLine($"Total: {total:0.00}");

public sealed record Order(string Number, decimal Amount);

public static class OrderMath
{
    public static decimal Total(IReadOnlyList<Order> orders)
    {
        decimal total = 0;
        for (var i = 1; i < orders.Count; i++) // bug: the first order is skipped
        {
            total += orders[i].Amount;
        }

        return total;
    }
}

Run it under the debugger from the CLI-built output in VS Code with a minimal launch.json entry:

{
  "name": "StepDemo",
  "type": "coreclr",
  "request": "launch",
  "program": "${workspaceFolder}/bin/Debug/net10.0/StepDemo.dll",
  "cwd": "${workspaceFolder}"
}

⚠ Common mistakes

  • Mistake: debugging a Release build without realising it. Optimised code merges and reorders lines, so breakpoints are skipped and variables show as "optimised away". Instead: debug logic bugs in the Debug configuration. Switch to Release only for a bug that only happens there.

  • Mistake: stepping into everything. Pressing F11 on every line walks you through property getters, LINQ and framework code you don't care about, and you lose track of the bug. Instead: Step Over by default, Step Into only the call you suspect (Step Into Specific when a line has several calls), and Step Out as soon as you are in the wrong method.

  • Mistake: pressing F5 again and again until the bad iteration. It is slow and easy to overshoot the case you need. Instead: use a conditional breakpoint, for example order.Number == "A-102", or a hit count.

  • Mistake: trusting state after Set Next Statement. Moving the instruction pointer skips code without running it, so variables, open transactions and side effects may be inconsistent. Instead: use it to re-run a pure calculation after changing a value. For anything with side effects, restart the scenario.

  • Mistake: ignoring a hollow breakpoint. It means the code you are looking at isn't the code that is running, so any conclusion you draw is wrong. Instead: hover for the reason, then check the Modules window for the module path and symbol status. Clean and rebuild if the binary is stale.

  • Mistake: chasing timeouts caused by the debugger. While paused, every thread stops, so HTTP and database timeouts expire, and the error you see after continuing is not the original bug. Instead: keep pauses short around I/O, raise timeouts for local debugging only, or use tracepoints and logs for timing-sensitive code.

  • Mistake: expecting async code to stay on one thread. After an await the method may resume on another thread, so the Threads window jumps and it looks like stepping is broken. Instead: use Step Over on the await line and let the debugger follow the continuation. Use the Tasks and Parallel Stacks windows to see waiting work.

  • Mistake: debugging a shared or production process interactively. A breakpoint pauses every request for every user. Instead: use logs, distributed traces, dotnet-counters and memory dumps in production, and reproduce locally to step through.

  • Mistake: committing Debugger.Break() or Debugger.Launch(). On a server or CI agent it can stop the process or pop up a debugger prompt. Instead: use normal breakpoints. If you need one in code temporarily, guard it with #if DEBUG and remove it before the pull request.

🏛 Senior insight: trade-offs

Seniors use the debugger to test hypotheses quickly, not to wander. Before pressing F5 they decide what they expect to see and where, place one or two targeted breakpoints, and use Step Out and Run to Cursor aggressively.

In multi-threaded and async code they adapt: a breakpoint stops every thread, so they check the Threads window to see which thread hit it, add a filter or condition so only the interesting request stops, and remember that after an await the method may resume on a different thread. For timing-sensitive bugs they switch to tracepoints or structured logs, because pausing changes the interleaving and can make the bug disappear.

They also know when the debugger is the wrong tool: production issues need logs, traces and dumps instead. A good habit is to turn what you learned at a breakpoint into a failing unit test before fixing, so the bug cannot come back.

🔗 Follow-ups the interviewer may ask

  1. When: The candidate says they press F5 repeatedly until the bad iteration. Why: Moves from basic breakpoints to stopping only when it matters.
    How to answer

    A plain breakpoint stops every time; the others let me stop only when it matters. A conditional breakpoint stops when an expression is true, such as order.Number == "A-102", or when a value changes. A hit count stops on, say, the 500th iteration. A filter limits it to one thread or process. A tracepoint, called a logpoint in VS Code, prints a message with values and keeps running, like a log line I don't have to commit. Function breakpoints stop on a method by name, and data breakpoints stop when a field changes. The cost is speed: conditions are evaluated by the debugger on every hit, so in a hot loop they can make the app crawl.

  2. When: The candidate mentions symbols, PDBs or a breakpoint that will not be hit. Why: Explains why breakpoints go hollow and how to debug into libraries.
    How to answer

    A PDB file maps compiled IL back to source lines and variable names; without it the debugger can't show source or bind breakpoints. Just My Code tells the debugger to step only through my code and treat everything else as external. To step into ASP.NET Core or a NuGet package, I turn Just My Code off, enable the Microsoft and NuGet.org symbol servers so the debugger can download PDBs, and enable Source Link. Source Link stores the repository URL and commit in the PDB, so the debugger fetches the exact source file that was compiled. Library authors make this work by publishing .snupkg symbol packages from deterministic CI builds.

↩ Usually asked after

More in Debugging Tools

Last updated 1 Nov 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me