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

📚 Interview question

How do you use the Watch, Locals, Autos and Immediate windows to inspect and change state while debugging, and what are the risks of evaluating expressions?

Inspecting and changing state at a breakpoint with Locals, Autos, Watch, QuickWatch, DataTips and the Immediate window (Debug Console in VS Code), format specifiers, Object IDs, and the side effects of function evaluation.

Intermediate Theory Intermediate debugging function-evaluation immediate-window visual-studio watch-window

🎯 Key points

  • Locals shows variables in the current scope; Autos shows variables used around the current line; Watch shows expressions you choose
  • The Immediate window (Debug Console in VS Code, Evaluate Expression in Rider) evaluates expressions and statements, calls methods and assigns values
  • Evaluating an expression runs real code in the paused process (function evaluation), which can have side effects
  • Use the `nse` format specifier to evaluate without side effects, and read backing fields instead of property getters with logic
  • Function evaluation can time out or deadlock because other threads stay frozen
  • Format specifiers (`nq`, `h`, `results`, `raw`) and Make Object ID help inspection
  • Changing a value at a breakpoint tests a hypothesis; it is not a fix

⏱ 30-second answer

At a breakpoint, Locals shows the variables in scope, Autos shows the ones used around the current line, and Watch shows expressions I pick and keep across steps. The Immediate window, which is the Debug Console in VS Code, lets me evaluate expressions, call methods and even assign new values to test an idea. The catch is that evaluating an expression runs real code in the paused process. A property getter with side effects, a LINQ query or a lazy-loaded EF Core navigation can change state or hit the database. When that matters I add the nse (no side effects) format specifier, as in customer.Orders, nse, or read backing fields directly. I also remember evaluation can time out because other threads are frozen.

🗣 Full answer (senior)

How I would answer this in an interview

"At a breakpoint, Locals shows the variables in scope, Autos shows the ones used around the current line, and Watch shows expressions I choose and keep across steps. The Immediate window, which is the Debug Console in VS Code or Evaluate Expression in Rider, lets me evaluate expressions, call methods and assign values to test an idea, and because a value only makes sense in the right frame, I use it alongside the Call Stack.

The catch is that evaluating anything other than a field or local is function evaluation: the debugger runs real code inside the paused process. A getter with side effects, a LINQ query or an EF Core lazy-loaded navigation can change state or hit the database, and the same applies to a DebuggerDisplay expression on the type. Because the other threads stay frozen, a method that needs a lock they hold can't finish, so the evaluation times out or deadlocks.

So when state is fragile I add the nse format specifier, as in customer.Orders, nse, or read the backing field instead of a property with logic. Other specifiers help too, such as nq, h, results and raw, and Make Object ID lets me follow one object after it leaves scope. The mistake I see is treating a value changed at a breakpoint as a fix, when it only tests a hypothesis.

The debugger is not a passive observer, so I change one value at a time, note what I changed, and turn the confirmed hypothesis into a test."


The windows

Window Shows Best for
Locals All locals and parameters in the current frame A quick overview after stopping
Autos Variables on the current and previous lines Following a few values while stepping
Watch 1–4 Expressions you add; they persist between sessions Tracking the same values over many steps
QuickWatch / DataTips One-off evaluation; hover tooltips that can be pinned to the editor Ad hoc checks
Immediate Free-form evaluation: ? expr, statements, method calls, assignments Testing a hypothesis without editing code
Parallel Watch The same expression across many threads or tasks Concurrency bugs

In VS Code the equivalents are the Variables and Watch panes and the Debug Console; in Rider, Variables, Watches and Evaluate Expression.

Useful tricks

  • Format specifiers after a comma in Watch: , h (hex), , nq (string without quotes), , results (force the Results View of an IEnumerable), , raw (ignore debugger display customisations), , nse (no side effects).
  • Make Object ID (right-click a value in Visual Studio) gives an object a handle like $1 that you can watch even after it goes out of scope, which is useful for following one object through a pipeline.
  • Changing values: edit a value in Locals or Watch, or assign in Immediate (order.Status = "Paid"), then continue to see whether the bug disappears.

The risks

Evaluation is function evaluation: the debugger runs a method inside your paused process. So:

  1. Side effects. A getter that increments a counter, starts a timer or caches a value changes the program you are debugging. Expanding an EF Core lazy-loaded navigation property issues a database query; expanding the Results View of an IQueryable runs it.
  2. Timeouts and deadlocks. Only one thread runs the evaluation while the others stay frozen. If the method needs a lock held by a frozen thread, it cannot finish and the debugger reports that the evaluation timed out. Some debuggers then stop evaluating automatically for the rest of the session.
  3. Misleading results. The Watch window may show a value computed at evaluation time, not the value the code actually saw.

Production example

While debugging an order service, a developer expanded customer.Orders in the Watch window on every step. With EF Core lazy-loading proxies, each expansion sent a query to the database and changed what the DbContext was tracking, so the bug moved. Switching to customer.Orders, nse and reading the change tracker made the real issue visible: a missing Include caused an N+1 pattern in production.

🔍 What the interviewer is checking

The interviewer is checking practical debugging fluency and awareness that evaluation changes the program.

Green flags

  • Knows the difference between Locals, Autos, Watch and Immediate.
  • Explains function evaluation, side effects and timeouts.
  • Mentions nse, results or Object IDs, and the EF Core lazy-loading trap.

Red flags

  • Believes the Watch window cannot affect the program.
  • Never uses the Immediate window or Debug Console.
  • Calls SaveChanges() from the Immediate window against shared data.

📚 Learn in depth

1. Reading memory versus running code

Every value you see in a debugger window comes from one of two places:

 expression in Watch: customer.Orders
      |
      v
 is it a local, parameter or field?
      |
      +-- yes --> debugger READS MEMORY directly. No code runs. Safe.
      |
      no (property getter, method call, ToString, DebuggerDisplay,
      |   LINQ, indexer with logic)
      v
 FUNCTION EVALUATION:
   borrow one stopped thread
   run the code inside your paused process
   (every other thread stays frozen)
      |
      +-- finishes      --> show the value
      +-- takes too long --> abort, show "evaluation timed out"
      +-- throws        --> show the exception as the value

That distinction explains every risk in this question. A field read can't change anything. Function evaluation is your code running for real.

2. Each window and what drives it

Window (Visual Studio) VS Code Rider What it shows Evaluates code?
Locals Variables Variables Every local and parameter in the selected frame Yes, to show properties when you expand objects
Autos (n/a) (n/a) Variables on the current and previous lines, plus return values of methods you just stepped over Yes
Watch 1 to 4 Watch Watches Expressions you choose; they persist between sessions Yes, on every step
QuickWatch, DataTips Hover Hover / Quick Evaluate One-off evaluation; DataTips can be pinned Yes
Immediate Debug Console Evaluate Expression Free-form expressions and statements Yes, including assignments and method calls
Parallel Watch (n/a) (n/a) One expression across many threads or tasks Yes, per thread

A Watch expression is re-evaluated on every step, so a side-effecting Watch runs its side effect every time you press F10.

3. Format specifiers

Add them after a comma in Watch, QuickWatch or Immediate:

Specifier Effect Example
nse No side effects: refuse to run anything that might change state customer.Orders, nse
ac Force evaluation of an expression the debugger skipped order.Total, ac
h / d Hexadecimal / decimal flags, h
nq String without quotes and escapes json, nq
results Show only the Results View of an IEnumerable query, results
raw Ignore DebuggerDisplay and type proxies order, raw
hidden Show all public and non-public members order, hidden
dynamic Show the Dynamic View of a dynamic object expando, dynamic

Useful pseudo-variables: $exception (the current exception), $ReturnValue (the value just returned), and Object IDs such as $1.

4. Make Object ID

Right-click a value and choose Make Object ID. The debugger keeps a weak handle to that specific object, named $1, $2 and so on.

 frame A: var order = Load("A-102");    --> Make Object ID --> $1
 frame B: Process(order)                --> watch $1.Status
 frame C: order is out of scope          --> $1.Status still works
 ...later, another variable 'x' looks the same
          ? object.ReferenceEquals(x, $1)  --> is it the SAME object?

It answers questions such as "is this the same instance or a copy?" and "what happened to this object after it left my method?". It doesn't keep the object alive; if it is collected, $1 shows that it is no longer available.

5. The three classic traps, step by step

Trap 1: a getter with a side effect.

 public IReadOnlyList<string> Numbers { get { _readCount++; return _numbers; } }

 Watch: batch.Numbers   --> _readCount++ on every step
 Your log later says "getter called 7 times" -- 5 of those were the debugger

Trap 2: lazy loading and IQueryable.

You expand What actually happens
customer.Orders with EF Core lazy-loading proxies A SQL query runs and the entities become tracked by the DbContext
The Results View of db.Orders.Where(...) The whole query runs against the database from your machine
query.Count() in Immediate Another SELECT COUNT(*)

The bug you were chasing (an N+1 pattern, a missing Include, a tracking problem) can disappear or change because the debugger loaded the data first.

Trap 3: deadlock during evaluation.

 Thread 1 (frozen) holds lock L
 Thread 5 (borrowed for evaluation) runs your getter, which needs lock L
   --> it waits forever --> evaluation times out
 Visual Studio may then disable further implicit evaluation for the session

The same happens with getters that call .Result on a task that needs a frozen thread to complete.

6. Changing state on purpose

You can edit a value in Locals or Watch, or run order.Status = "Paid" in the Immediate window. Use it as an experiment:

  1. Change one thing.
  2. Write down what you changed (and tell your pair).
  3. Continue and observe.
  4. Treat everything after that point as an experiment, not real behaviour.
  5. If it goes wrong, restart the session instead of trying to undo it by hand.
  6. Once confirmed, change the code and add a test. A value changed in the debugger is a hypothesis, not a fix.

Remember the Immediate window runs real code: db.SaveChanges() saves to the database, and httpClient.PostAsync(...) sends a real request.

7. Settings that control evaluation (Visual Studio)

Setting (Tools > Options > Debugging > General) Effect
Enable property evaluation and other implicit function calls Turn off to stop the debugger running getters automatically; values then show a refresh icon you click to evaluate
Call string-conversion function on objects in variables windows Turn off if ToString() overrides are slow or have side effects

Turning these off is a good idea when debugging code with fragile state or expensive getters.

8. Security and privacy

Watch and Immediate show whatever is in memory: tokens, connection strings, customer records and personal information. Debug with test data, don't share screen recordings of sessions on real data, and remember that pinned DataTips and Watch expressions are saved in your user settings for the solution.

9. .NET 11 awareness

These windows are IDE features and work the same for .NET 10 and .NET 11 previews. The expression evaluator follows new C# syntax as each IDE release adds support.

10. Try it yourself

  1. Run the code example to the breakpoint line.
  2. Add batch.Numbers to Watch, step a few times, and watch batch.ReadCount climb even though your code didn't read it.
  3. Change the Watch to batch.Numbers, nse and read the message.
  4. Add batch._numbers instead and confirm it shows the list without changing the count.
  5. In the Immediate window run batch.Add("A-999"), then ? batch.Numbers.Count.
  6. Right-click batch, choose Make Object ID, step out of the method and watch $1.
  7. Turn off implicit property evaluation in the options and see the refresh icons appear.

💻 Code

A class whose property has a side effect:

var batch = new OrderBatch();
batch.Add("A-100");
batch.Add("A-101");

Console.WriteLine($"Orders: {batch.Numbers.Count}"); // set a breakpoint here
Console.WriteLine($"Getter called {batch.ReadCount} time(s)");

public sealed class OrderBatch
{
    private readonly List<string> _numbers = [];
    private int _readCount;

    public IReadOnlyList<string> Numbers
    {
        get
        {
            _readCount++; // side effect: watching this property changes state
            return _numbers;
        }
    }

    public int ReadCount => _readCount;

    public void Add(string number) => _numbers.Add(number);
}

Watch expressions and Immediate window commands to try:

batch.Numbers, nse          // refuses to run the getter
batch._numbers              // reads the backing field directly, no code runs
batch.ReadCount, h          // hexadecimal
batch, raw                  // ignores any DebuggerDisplay customisation

? batch.Numbers.Count       // Immediate: evaluate (runs the getter)
batch.Add("A-999")          // Immediate: call a method in the paused process

⚠ Common mistakes

  • Mistake: assuming watching is read-only. Getters, ToString(), DebuggerDisplay, LINQ and indexers all run code inside your process, and a Watch expression re-runs on every step. Instead: watch fields and locals where possible, add , nse when state is fragile, and turn off implicit property evaluation for code with side-effecting getters.

  • Mistake: expanding an IQueryable Results View or a lazy-loaded navigation against a real database. The debugger sends the SQL. On a large table that is a slow, full query from your machine, and with tracking it also loads entities into the DbContext, changing what SaveChanges() will do and hiding N+1 problems. Instead: use , nse, inspect db.ChangeTracker or the generated SQL in logs, and debug against a local database with test data.

  • Mistake: calling methods that need locks or async results from Watch or Immediate. Other threads are frozen, so a method waiting for a lock or a task they own never finishes. The evaluation times out and the debugger may stop evaluating for the rest of the session. Instead: evaluate simple field reads. If you need the result of a complex method, step to a point where your code calls it.

  • Mistake: treating a value changed in the debugger as proof of a fix. Changing state at a breakpoint shows what would happen if the value were different, not why it was wrong. Instead: use it to confirm the hypothesis, then fix the code and add a test that fails without the fix.

  • Mistake: making several changes at once. If the bug disappears, you don't know which change mattered, and the process is now in a state real code never produces. Instead: change one value at a time, note it, and restart the session when an experiment goes wrong.

  • Mistake: not telling the team what you changed. When pairing or reproducing a bug together, an unmentioned edit in Locals or the Immediate window makes everyone draw the wrong conclusion. Instead: say it aloud, or note it in the ticket alongside the reproduction steps.

  • Mistake: running state-changing commands in the Immediate window. SaveChanges(), sending emails or calling an API from the Immediate window does it for real. Instead: keep the Immediate window for reading values and small experiments on local data. Write a test or a scratch app for anything that changes external state.

  • Mistake: losing track of a value as Autos changes. Autos only shows variables near the current line, so values disappear as you step. Instead: add the value to Watch, or use Make Object ID ($1) to keep following one object after it leaves scope.

  • Mistake: applying local debugging habits to production. These windows need a live, paused process, and pausing production stops every request. Instead: answer production questions with logs, traces, dotnet-counters and memory dumps.

  • Mistake: debugging on production data. Watch windows, pinned DataTips and screenshots expose personal information and secrets. Instead: debug with test or anonymised data, and never share recordings of sessions on real customer data.

🏛 Senior insight: trade-offs

A senior engineer knows the debugger is not a passive observer. They prefer fields and nse when state is fragile, avoid evaluating anything that does I/O, and keep production-like data out of debug sessions.

When they do change state on purpose, they do it deliberately: change one value, write down what they changed, and treat everything after that point as an experiment rather than real behaviour. If an experiment goes wrong, they restart the session instead of trying to undo it by hand. For questions that need repeated mutation, they write a small unit test or a scratch console app instead of driving the real app from the Immediate window.

They use the Immediate window as a fast experiment loop ("if this value were X, would the bug go away?") and then encode the answer as a test, because a hypothesis confirmed in the debugger is not a fix until code and tests change. On a team they make types debugger-friendly with cheap, side-effect-free DebuggerDisplay expressions so everyone's Watch window is useful.

🔗 Follow-ups the interviewer may ask

  1. When: The candidate mentions DebuggerDisplay or customised Watch output. Why: DebuggerDisplay is evaluated just like a Watch expression, so the same risks apply to type authors.
    How to answer

    These attributes change only what the debugger shows; they don't change runtime behaviour. [DebuggerDisplay] sets the one-line summary in Watch and DataTips, so it should be cheap and side-effect free. The usual pattern is a private DebuggerDisplay property with nq. [DebuggerBrowsable(Never)] hides noisy fields, and [DebuggerTypeProxy] gives a custom view. [DebuggerStepThrough] skips trivial code when stepping, but overused it hides bugs. In this class I'd remove the email and password hash from the display, replace the display expression that calls the database, make the side-effecting property a method, and delete the leftover Debugger.Launch().

  2. When: The candidate inspects a value and asks where it came from. Why: Watch values only make sense in the right frame, so the next skill is navigating the stack.
    How to answer

    The Call Stack shows every method that is currently active on the thread, newest at the top: who called whom to reach this line. I double-click a lower frame to jump to that caller, and Locals and Watch switch to that frame's variables, which is how I find where a bad value came from. Just My Code hides framework frames as [External Code]. For async code the real thread stack after an await starts in the thread pool, so Visual Studio stitches the logical async chain back together, and Parallel Stacks shows it across tasks. In Release builds, inlining can hide frames.

↩ Usually asked after

More in Debugging Tools

Last updated 1 Nov 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me