Debugging C# Programs
Separate compiler errors, runtime failures, and wrong results, then use evidence to locate causes instead of guessing at fixes.
Before this lesson
Distinguish compile-time, runtime, and logic problems
Read diagnostics from the first relevant error
Use breakpoints, stepping, watches, and the call stack deliberately
Trace changing state and test boundary inputs
The short answer
Debugging is the process of finding why observed behavior differs from expected behavior. First classify the problem, reproduce it with a small input, inspect the earliest useful message or state, test one hypothesis, and verify the fix against normal and boundary cases.
Different failures require different questions
A compile-time error prevents a program from being built: syntax is invalid, a name is unknown, or types do not fit. A runtime exception occurs after execution starts, such as indexing beyond an array. A logic error produces a result but the result is wrong. The compiler cannot know that a discount rule or average formula contradicts your requirement.
Classifying the symptom narrows the investigation. For a compiler error, inspect the message and nearby code. For an exception, read its type, message, and stack trace. For wrong output, compare expected and actual state at each important step.
| Problem | Typical symptom | First move |
|---|---|---|
| Compile-time | No executable result | Read the first diagnostic and location |
| Runtime | Exception after starting | Read exception type and stack trace |
| Logic | Program finishes with wrong output | Trace values against a hand-worked example |
Start with a reproducible case
A useful bug report says which input produces which result and what should happen instead. ‘The total is broken’ is difficult to test. ‘Quantities 2 and 3 produce 5.00, but with price 4.00 the total should be 20.00’ gives you a repeatable case and an expected value.
Reduce the input while preserving the failure. Small cases are easier to calculate by hand and produce less output. Do not change five lines at once; each change creates another possible cause.
Control execution with a debugger
A debugger pauses a running program without requiring you to edit its output. Set a breakpoint on an executable line near the suspected failure, start debugging, reproduce the input, and inspect the program state before changing code. The highlighted line is normally the next statement to execute, not one that has already completed.
Use Step Over when you want a method call to run as one operation. Use Step Into when the bug may be inside that method, and Step Out when you have learned enough and want to return to the caller. A Watch keeps a chosen variable or expression visible; the Call Stack shows which chain of methods led to the current line. Shortcut keys vary by editor, so learn the commands as concepts first.
| Debugger control | Question it answers | Useful moment |
|---|---|---|
| Breakpoint | What is true when execution reaches this line? | Pause before a risky branch or update |
| Step Over | What changes after this statement finishes? | Stay focused on the current method |
| Step Into | What happens inside this call? | Follow a suspicious method |
| Step Out | What does this method return to its caller? | Leave code already understood |
| Watch | How does this expression change? | Track a counter, total, or condition |
| Call Stack | How did execution get here? | Trace nested method calls or an exception |
Trace the smallest useful state
Temporary Console.WriteLine statements can reveal the value before and after an update. A debugger provides breakpoints, step controls, watches, and variable inspection without changing output. Both approaches answer the same question: what did the program know at this moment?
For loops, print the counter, condition-relevant state, and accumulated result. For conditions, print the smaller Boolean facts. Remove noisy diagnostic output after the cause is understood, or replace it with deliberate application logging when it has ongoing operational value.
int total = 0;
for (int number = 1; number <= 3; number++)
{
total += number;
Console.WriteLine($"number={number}, total={total}");
}Expected output
number=1, total=1 number=2, total=3 number=3, total=6
Quick knowledge check
Answer before you reveal.
01A program compiles and prints 12 instead of the expected 10. Which broad category is this?
A logic error: execution completed, but the implemented rule produced the wrong result.
02Why read the first compiler error before the tenth?
Later errors can be cascading consequences of an earlier missing token or invalid declaration.
Exercise
Practice challenge
The loop for (int i = 0; i <= names.Length; i++) crashes while printing an array. Reproduce the failure, predict the invalid index, then use either a debugger breakpoint or temporary trace output to confirm it. Correct the loop and test arrays containing zero, one, and three names.
Requirements
- You can name the exception category and the invalid index
- The correction comes from the valid index range rather than a hardcoded length
- All three test arrays behave correctly and every name prints exactly once
- You remove temporary tracing or breakpoints after verifying the correction
Optional extension: Put the loop in a PrintNames method, step into it from Main, and use the call stack to identify the caller.
Open in C# compilerLesson checkpoint
One small step locks it in
Mark this lesson complete, then keep the momentum going.
Clear up the details
Frequently asked questions
Is debugging the same as testing?
Testing looks for evidence that behavior meets requirements; debugging investigates a known mismatch. They support each other but are not identical.
Should I catch every exception to keep the program running?
No. Catch only where you can add context, recover, or translate the failure meaningfully. Hiding an unexpected exception can corrupt state and make diagnosis harder.
When should I use a debugger instead of print statements?
Use a debugger when you need to step through branches or inspect several values without changing output. Print tracing remains useful in constrained environments and for a quick small case.