Error Thinking
Before you start
You need: a programming track in progress. Take this alongside your first project, not after it.
Time: about 45 minutes.
Learning objective
Turn "it doesn't work" into a specific, reproducible statement, and narrow the cause by testing one hypothesis at a time.
Topics
- Why debugging is a method, not a talent
- Reproducing before fixing
- Reading an error message
- Stack traces
- Hypothesis and test
- Bisecting the problem space
- Rubber-ducking
- Knowing when to stop
Debugging is a method
Most people debug by changing things until the symptom stops. It works occasionally, and it produces code nobody understands — including the person who "fixed" it.
The method is four steps, in order:
| Step | Question |
|---|---|
| Reproduce | Can I make it happen on demand? |
| Locate | Which part of the system is wrong? |
| Understand | Why does it produce this behaviour? |
| Fix | What is the smallest correct change? |
Skipping straight to step 4 is the defining beginner mistake. A fix applied without understanding either does nothing, or hides the symptom while the cause remains.
A bug you cannot reproduce is a bug you cannot verify as fixed. You can only claim it stopped happening while you watched.
Reproducing
"The fee page is broken" is not a bug report. This is:
Steps:
1. Sign in as priya.sharma@nca.test (Admin, School 1)
2. Open /students/NCA-2024-0012/fees
3. Click "Print receipt"
Expected: A receipt showing ₹12,000 paid
Actual: 500 error, page stays blank
Frequency: Every time, for this student only
Also: Works for NCA-2024-0013
Since: After yesterday's deploy
"For this student only" is the most valuable line in that report. It converts an open-ended investigation into a comparison: what is different about NCA-2024-0012?
Questions that narrow a bug fast:
- Does it happen for every user, or one?
- Every record, or one?
- Every environment, or only production?
- Every browser, or one?
- Did it ever work? When did it stop?
- Does it happen on a fresh login?
"It works on my machine" is data, not a defence. It means the difference between the two environments is the bug's cause — configuration, data, version or permission.
Reading an error
Beginners skim errors. The message usually names the cause.
System.NullReferenceException: Object reference not set to an instance of an object.
at NexCoding.SchoolPortal.Services.FeeService.CalculateTotalFees(Int32 studentId) in D:\src\Services\FeeService.cs:line 47
at NexCoding.SchoolPortal.Controllers.FeeController.GetSummary(Int32 studentId) in D:\src\Controllers\FeeController.cs:line 28
Four facts are stated outright:
| Fact | Value |
|---|---|
| Type | NullReferenceException — something was null |
| Where | FeeService.cs, line 47 |
| Called from | FeeController.GetSummary, line 28 |
| Input | studentId |
Go to line 47 and ask which reference on that line can be null.
// line 47
return account.TotalFees - account.DiscountAmount;
account is null — the student has no fee account. The exception was not mysterious; it was specific.
Common .NET messages and what they actually mean:
| Message | Meaning |
|---|---|
NullReferenceException | Used something that was never assigned, or a lookup returned nothing |
InvalidOperationException: Sequence contains no elements | .First() on an empty result — use .FirstOrDefault() and check |
SqlException: Invalid column name 'X' | Schema and code disagree — migration not applied |
SqlException: Timeout expired | Query too slow, or blocked by another transaction |
InvalidCastException | Column type does not match the property type |
FormatException | Parsing a string that is not in the expected format |
ObjectDisposedException | Used a connection or context after its using block ended |
Read the innermost exception. In .NET the useful one is often the InnerException, several layers down:
catch (Exception ex)
{
Exception root = ex;
while (root.InnerException != null)
{
root = root.InnerException;
}
_logger.LogError(root, "Failed to calculate fees for student {StudentId}", studentId);
}
Search the exact message, minus your own identifiers. Search Sequence contains no elements, not Sequence contains no elements for student NCA-2024-0012.
Stack traces
A stack trace reads bottom to top in call order — the bottom is where execution started, the top is where it failed.
at FeeService.CalculateTotalFees(Int32 studentId) ← failed here
at FeeController.GetSummary(Int32 studentId) ← called from here
at lambda_method(Closure, Object, Object[]) ← framework
at ControllerActionInvoker.InvokeActionMethodAsync() ← framework
Find the topmost frame in your own code. Framework frames above it are usually noise; the first line with your namespace and a file path is where to look.
An async stack trace is shorter and less useful, because awaits break the chain. Log the parameters at the point of failure rather than relying on the trace alone.
Hypothesis and test
State what you believe, then design the cheapest test that could prove it wrong.
Hypothesis: The student has no FeeAccount row, so the lookup returns null.
Test: SELECT * FROM FeeAccount WHERE StudentId = 12;
Result: 0 rows.
Conclusion: Confirmed. The code assumes every student has an account.
Hypothesis: The token has expired, so the API returns 401.
Test: Decode the token and read the exp claim.
Result: exp is two hours in the future.
Conclusion: Rejected. Look elsewhere.
Rejecting a hypothesis is progress. It removes a possibility permanently, which guessing never does.
Change one thing at a time. Change three, see the symptom go, and you do not know which one mattered — or whether you introduced a second bug that cancels the first.
Write down what you have ruled out. An hour into a hard bug, you will otherwise retest the same things.
Bisecting the problem space
The single most effective technique for a bug with no obvious cause: cut the system in half and determine which half contains the fault.
For a full-stack failure:
Browser → Frontend JS → Network → API → Service → Repository → SQL Server
Ask one question at each boundary:
| Check | Tells you |
|---|---|
| Does the request appear in the Network tab? | Frontend sent it, or did not |
| What status code? | Which side owns the failure |
| Does the same request work in Postman? | Frontend problem or API problem |
| Does the query work in SSMS? | Application problem or SQL problem |
| Does the API log show the call arriving? | Routing, or the handler |
Three or four questions locate almost any full-stack bug, because each one halves the remaining surface.
For a bug in a single method, the same idea applies to the code: log or breakpoint at the midpoint, and see whether the values are already wrong there.
For a regression, git bisect does this over history.
Rubber-ducking
Explain the problem out loud, in full, to a colleague or an inanimate object.
It works because explaining forces you to state the assumptions you have been skipping. "It gets the student, then the fee account, then subtracts the discount — wait, it doesn't check whether the account exists."
Most developers have solved a bug mid-sentence while asking for help. That is the technique working, not a coincidence.
Write the explanation if nobody is around. A written bug report to yourself does the same job.
Knowing when to stop
After 30 minutes with no progress, change something about the approach, not the code:
- Take a break. Genuinely — the answer arriving in the shower is a real effect, not folklore.
- Explain it to someone.
- Reread the error message from the beginning.
- Question an assumption you have been treating as fact — "the config is right", "the deploy went out", "that table has data".
- Check the obvious: is the service running? Is it the right environment? Did you save the file?
After two hours, ask for help. Bring the reproduction steps, the error, and what you have ruled out. That is a five-minute conversation instead of an hour of someone else's debugging.
Struggling silently for a day is not diligence — it is an expensive way to learn something a colleague could have told you.
Errors you will hit
| What you say | What it should be |
|---|---|
| "It doesn't work" | Steps, expected, actual, frequency, scope |
| "It works on my machine" | Which difference between the environments explains it |
| "It fails randomly" | Which users, which records, what time, under what load |
| "I fixed it" | Reproduced first, understood, fixed, verified |
| "There's no error" | Logical error — found by testing, not by tooling |
"Random" almost always means a condition you have not identified yet.
Common mistakes
- Fixing before reproducing
- Skimming the error message
- Reading the outer exception and not the inner one
- Changing several things at once
- Not recording what has been ruled out
- Treating "works on my machine" as an answer
- Assuming rather than checking that a config value is set
- Guessing instead of bisecting the system
- Debugging silently for a day
- Fixing the symptom without understanding the cause
Practice
The course exercise is describe and narrow a bug.
- Take a bug from a past project and write a proper report with steps, expected, actual, frequency and scope.
- Trigger a
NullReferenceExceptiondeliberately. Read the trace and identify the exact null reference before looking at the code. - Trigger
Sequence contains no elementswith.First()on an empty query. Fix it correctly rather than wrapping it in try/catch. - Wrap an exception in two layers and write the loop that finds the innermost one.
- Take a stack trace and identify the topmost frame in your own code.
- For a bug you already understand, write the hypothesis and the test that would have proved it, then confirm the test works.
- Deliberately reject a hypothesis and record what it ruled out.
- Take a full-stack failure and locate it with four boundary questions — Network, status code, Postman, SSMS.
- Change three things at once to fix something, then revert and find which one mattered.
- Explain a current bug out loud, in full, and note whether you found something mid-explanation.
- Keep a written log for one bug: hypotheses, tests, results, what was ruled out.
- Set a 30-minute timer on the next bug. When it fires, change approach rather than continuing.
Exercise 11 is the habit worth keeping. A written log is the difference between debugging and thrashing.
You can now
- Turn a vague complaint into a reproducible report
- Read an error message and a stack trace for what they state
- Form a hypothesis and design a test that could reject it
- Bisect a system rather than reading all of it
- Know when to stop and ask
Review questions
- Why must you reproduce a bug before fixing it?
- What four facts does a .NET stack trace give you?
- What makes bisecting the system faster than reading code?
- Why is changing one thing at a time not just tidiness?
Next: Debugging code