Dataslope logoDataslope

Debugging and Reasoning About Programs

Finding bugs by thinking clearly — stack traces, mental tracing, hypotheses, and invariants

Every program you write will have bugs. That's not a personal failing; it's the human condition. What separates beginners from experienced developers isn't fewer bugs — it's how quickly and calmly they find them.

This chapter is less about syntax and more about the method of debugging — a mental discipline you can practice on every program.

A bug is a gap between two stories

Whenever a program misbehaves, you really have two stories:

  1. What you think the program does.
  2. What the program actually does.

A bug is the place where those two diverge. Debugging is the work of figuring out which step in your mental story doesn't match reality — and then fixing one or the other.

The scientific method, applied

Every productive debugging session looks like this:

  1. Observe. Read the actual symptom — error message, wrong output, hang. Don't guess; look.
  2. Hypothesize. Form one specific theory: "I think total is wrong before line 27 because the loop never runs."
  3. Test. Make ONE change — a print, a breakpoint, an extra assertion — that would prove or disprove your hypothesis.
  4. Compare the result to what you expected.
  5. Refine. Either narrow the suspect zone or update the hypothesis.

The single biggest beginner mistake is to change five things at once and hope something works. That doesn't teach you anything, even if the symptom goes away.

Reading a stack trace

When an unhandled exception is thrown, C# prints a stack trace showing the chain of method calls that led to the failure. Read it from the top:

System.DivideByZeroException: Attempted to divide by zero.
   at SimpleMath.Divide(Int32 a, Int32 b) in /src/SimpleMath.cs:line 7
   at Program.<Main>$(String[] args) in /src/Program.cs:line 5

This is gold:

  • What broke: DivideByZeroException.
  • Where it broke: SimpleMath.Divide, line 7.
  • Why we got there: Program.<Main>$ called it at line 5.

The vast majority of "I can't figure this out" cases evaporate when you stop and actually read the trace top to bottom.

You don't need a debugger to find most bugs. A handful of well-placed Console.WriteLines often gets you there faster.

Code Block
C# 13

Run it. The trace makes the bug obvious — the loop stops at i = 4 when we expected it to include 5. The fix is i <= n.

After fixing, delete the prints. Leftover diagnostic prints become noise that hides the next bug.

Mental tracing: pretending to be the CPU

Pick a small input and walk through the code line by line on paper, tracking each variable's value. It's tedious — and it works. Most off-by-one bugs and most logic bugs surface within five lines of mental tracing.

Try it for this snippet, then run it:

Code Block
C# 13

That program happens to give the right answer for this input, because the array contains a value bigger than 0. But what if the array were { -5, -2, -8 }? The bug is hiding behind a friendly input.

The fix is to initialize max from the array itself: int max = arr[0];

Invariants: facts that should always be true

An invariant is a property of your program that must hold at some point: "after Withdraw, Balance >= 0", "the list is sorted", "this index is between 0 and length".

Writing them down (in comments, or as assertions) is one of the single most useful debugging habits.

Code Block
C# 13

Assertions document your assumptions and catch them being violated as soon as something goes wrong, instead of much later when the damage is harder to trace.

Common bug categories — what to look for first

SymptomSuspect
Off-by-one in a loopWrong comparison (< vs <=), wrong start
NullReferenceExceptionA variable that should have been initialized
IndexOutOfRangeExceptionA loop runs one too many times
Wrong total / wrong countInitializer wrong, or condition skips items
Code "doesn't run"Wrong branch taken; check the if condition
Random crashes between runsUninitialized data or shared mutable state

Knowing the shape of common bugs lets you skip straight to the likely cause.

Practice — a tiny bug hunt

The function below is supposed to return the largest value in the array. It has a bug. Run it, observe the wrong answer, fix it, and verify.

Code Block
C# 13

The first call prints 0 — clearly wrong. The bug is the initial value of max. Initialize it to xs[0] and try again.

Test your understanding

QuestionSelect one

What's the most useful first step when a program crashes with an exception?

Add a try/catch to make it go away

Run it a few times to see if the problem repeats

Read the stack trace from the top — the exception type and the file/line tell you what went wrong and where

Reinstall the .NET SDK

QuestionSelect one

Why is it a bad idea to change five things at once when debugging?

It might break the build

Even if the symptom goes away, you don't know which change fixed it, so you've learned nothing and may have introduced new bugs

The compiler refuses to compile multi-change files

Git makes it impossible

On this page