A Field Guide

Debugging

A systematic method for finding bugs instead of guessing at them — from reading error messages to isolating causes to knowing what to do when you're completely stuck.

🧠 Part 1: Mental Model
📖 Part 2–3: Reading & Reproducing
🔍 Part 4: Isolation
🐛 Part 5: Common Patterns
🤖 Part 6–7: AI Code
🧪 Part 8–9: Recovery & Prevention
🌐 Part 10–11: Browser & Psychology

Who This Is For

Anyone who writes code and gets stuck. You don't need to be an experienced developer — in fact, this guide is especially useful if debugging still feels like guessing. No prior knowledge is assumed beyond basic familiarity with writing and running code.

This is a 40-minute read. By the end, you'll have a systematic method for finding bugs instead of guessing at them, know how to read error messages and logs like a native, understand the failure patterns that show up over and over, and have specific techniques for debugging code written by AI — which fails in distinctive ways that aren't well-covered elsewhere.

Why Debugging Feels Hard (and Why It Doesn't Have To)

Most people debug the same way: they stare at the code, change something that feels suspicious, run it again, and hope the error goes away. When it doesn't, they change something else.

This is what debugging by intuition looks like, and it works sometimes — when you're lucky, or when you've seen this exact bug a hundred times before. But it scales badly. When the bug is subtle, when the system is complex, when you've been staring at the same code for an hour — intuition fails.

The alternative is treating debugging as a scientific method: forming hypotheses about what's wrong, finding ways to test those hypotheses, and systematically eliminating possibilities until you've isolated the cause.

💡 The Method
It's more structured, it scales to complex problems, and it produces a satisfying click of understanding when you find the bug — rather than the vague relief of "it seems to be working now."
Part One
Bugs as Information
What a bug actually is, and why diagnosis must come before treatment.

What a Bug Actually Is

A bug is a gap between what the code does and what you intended it to do. That framing matters, because it tells you there are always two things to examine: the code's actual behavior, and your mental model of what it should do.

Sometimes the code is wrong. Sometimes your mental model is wrong. Sometimes both are.

The first job of debugging isn't to fix the code — it's to understand the gap precisely.

The Scientific Method Applied to Code

Debugging has the same structure as scientific investigation:

  1. Observe — what is actually happening?
  2. Hypothesize — what would explain this?
  3. Predict — if my hypothesis is right, what else should I observe?
  4. Test — design an experiment to check
  5. Conclude — update your hypothesis based on results
⚠️ The Common Mistake
Most debugging goes wrong at step 2: people form one hypothesis and immediately jump to "fixing" it, without testing it first. Then when the fix doesn't work, they don't know if their hypothesis was wrong or their fix was wrong.

The discipline is: don't touch the code until you understand the problem. Diagnosis first, then treatment.

Bugs Don't Lie, but They Can Mislead

An error message tells you what went wrong from the system's perspective. That's not always where the actual problem is. A null pointer error in function B might be caused by function A returning null when it shouldn't. The symptom (crash in B) points at the wrong place.

The bug is always real — but the symptom and the cause are often in different places. Your job is to trace from symptom back to cause.

The Three Types of Bugs

Syntax errors — the code can't be parsed. Your editor usually catches these before you run the code.

Runtime errors — the code runs but crashes. An error is thrown, and you get a stack trace. These are usually the easiest to debug because the error is explicit.

Logic errors — the code runs without crashing, but produces wrong results. No error is thrown; the program does something other than what you intended. These are the hardest, because the system isn't telling you something's wrong — you have to notice it yourself.

📝 The Spectrum
Syntax errors are caught by tools. Runtime errors announce themselves. Logic errors hide. As you get better at debugging, the bugs you encounter shift from the first category toward the last — which is why the systematic method matters more, not less, as you gain experience.
Part Two
Reading Error Messages
The skill everyone skips — and the fastest path to understanding what went wrong.

The Anatomy of an Error Message

Error messages are trying to tell you something. Most people scan them for a few words and then start guessing. That's backwards. The error message is often telling you almost exactly what's wrong.

Most error messages have the same structure, even across different languages and frameworks:

What happened: The error type and a brief description.

TypeError: Cannot read properties of undefined (reading 'name')

A TypeError, trying to read a property called 'name' on something that was undefined.

Where it happened: A file path and line number.

at getUserName (users.js:47:12)

In the function getUserName, in the file users.js, on line 47.

How we got here: The stack trace — a list of function calls that led to the error, from most recent to oldest. The top of the stack is where the error occurred; further down is where it was called from.

The Most Common Error Types

Error TypeWhat It Means
TypeErrorUsing a value as if it's something it isn't. Calling a function on null. Accessing a property on undefined.
ReferenceErrorTrying to use a variable that doesn't exist. Usually a typo or scope issue.
SyntaxErrorCode can't be parsed. Missing bracket, parenthesis, or semicolon.
IndexError / KeyErrorAsking for something at a position or key that doesn't exist.
Import / Module Not FoundModule isn't installed, or the path is wrong.

When the Error Message Seems Useless

Sometimes you get an error that's technically accurate but unhelpful — a generic "something went wrong" from a library that swallows the real error. In these cases:

  • Look for a "caused by" section lower in the error
  • Check if the library has a debug mode or verbose logging
  • Search for the error message exactly as written — someone else has seen it

Reading the Stack Trace

New programmers often read only the first line of a stack trace (the error) and the second line (where it crashed). But the stack trace tells a story.

Often the most important clue is several lines down, in your own code, before the execution entered a library or framework.

💡 The Rule
Look for the first line in the trace that references your code (a filename you recognize). That's usually where you should start looking — the error propagated up from there.
Part Three
Reproducing the Bug
The most important step — you can't fix what you can't reliably trigger.

Why Reproduction Matters

Before you can fix a bug, you need to be able to reproduce it reliably. If you can't reproduce a bug, you can't test whether a fix works.

You might change three things, the bug goes away, and you don't know which change fixed it — or if it just stopped happening by coincidence. Reliable reproduction gives you the ability to run an experiment. It transforms debugging from guessing into testing.

Creating a Minimal Reproduction

The goal is to find the smallest, simplest version of the code that still triggers the bug. This is called a minimal reproducible example (MRE).

Start with your failing case and remove things. Can you remove this function call? Does the bug still happen? Remove this parameter? Still happens? Each thing you remove that doesn't affect the bug is something you don't have to think about.

💡 The Insight
When you've stripped it down, you often understand the bug just from doing the stripping — the act of isolation reveals the cause.

A minimal reproduction is also what you share when asking for help. "My app crashes when I click this button" is not useful. A fifty-line file that demonstrates the crash is.

Flaky Bugs — Intermittent Failures

Some bugs only happen sometimes: under specific timing conditions, with specific network latency, when the database is under load. These are the hardest class of bug because you can't rely on reproducing them.

For flaky bugs: add logging to capture what's happening when it fails. Look for patterns — does it fail more under load? On specific inputs? After a certain amount of time? The pattern usually points at a race condition, a timeout, or an assumption that doesn't hold in all environments.

⚠️ Production-Only Bugs
If a bug only happens in production and not locally, suspect: environment variables or config differences, data in production that doesn't exist in development, differences in scale or timing, or external services behaving differently.
Part Four
Isolation
Finding where the problem actually is — binary search, print statements, debuggers, and logging.

Using a Real Debugger

A debugger lets you pause execution at any point, inspect all variables at that moment, and step through code one line at a time. It's slower to set up than print statements but more powerful for complex bugs.

Every major language has one built in or available. Python has pdb (built in) and better alternatives like ipdb. JavaScript has the browser DevTools debugger and Node's built-in one. Most IDEs include visual debuggers.

Key Debugger Techniques

TechniqueWhat It Does
BreakpointsPause when execution reaches a specific line. Inspect what's in memory.
Step overExecute the current line and pause on the next one.
Step intoIf the current line calls a function, follow execution into that function.
Step outRun the rest of the current function and pause when it returns.
Watch expressionsContinuously display the value of a specific variable as you step.
💡 When to Use Which
For complex bugs — especially logic errors where nothing crashes but results are wrong — a debugger is often faster than print statements.

Logging

For bugs that happen in production, or intermittently, or in code you can't easily step through, logging is the equivalent of print statements that run in the wild.

Good logging:

  • Records when important things happen (function called, external call made, decision taken)
  • Captures relevant context (user ID, request ID, the values being operated on)
  • Has meaningful levels (DEBUG for verbose detail, INFO for normal operation, WARN for things that might be problems, ERROR for things that are)
  • Includes timestamps
⚠️ Bad Logging
Logging everything at DEBUG in production (drowns out signal), logging nothing (can't diagnose production issues), logging without context ("error occurred" tells you nothing).
Part Five
Common Bug Patterns
The usual suspects — the same patterns that have appeared in code since the beginning of software.

The Usual Suspects

Most bugs aren't novel. They're variations of the same patterns that have appeared in code since the beginning of software. Knowing these patterns by name speeds up diagnosis.

Off-by-One Errors

You loop through indices 0 to n when you meant 0 to n-1. You check < length when you should check <= length, or vice versa. You skip the first element, or process one element too many.

When your results are "almost right but shifted by one," think off-by-one.

Null and Undefined

Accessing a property on something that turned out to be null or undefined. This is the most common runtime error in most languages. It almost always means either (a) something didn't initialize that was supposed to, or (b) you're using a value before it's been set.

💡 The Real Fix
Adding a null check fixes the symptom. But the real fix is to ask: why is this null when I expected it not to be? The null is a symptom — trace back to where it was supposed to be set.

Wrong Type

Treating a string as a number, or vice versa. In dynamically typed languages, these are very common. The variable is named count and you assume it's a number, but somewhere upstream it got serialized to a string. Now count + 1 is "51" instead of 6.

# Print the type, not just the value
print(type(my_variable))           # Python
console.log(typeof myVariable)     # JavaScript

Mutation When You Meant a Copy

You passed an object into a function, the function modified it, and now the original is changed — which you didn't intend. In many languages, objects are passed by reference, meaning the function operates on the same object in memory, not a copy.

If you're seeing state changes that seem to happen from somewhere you didn't expect, check whether an object is being mutated when a copy was intended.

Race Conditions

Two operations happen in an order you didn't account for. A file is read before it's finished being written. A database query runs before a previous update has committed. An async function resolves in a different order than you assumed.

The signature is: "it works fine locally but breaks occasionally in production" or "it works fine when I run it slowly but breaks when I add load."

Silent Failures

A function returns an error, and you don't check the return value. An exception is caught somewhere and swallowed silently. An API call fails and returns an empty result that gets processed as if it were valid data.

When results seem wrong with no errors, look for places where errors might be caught and silenced.

Cached Stale Data

You're operating on data that was true ten minutes ago but isn't anymore. A cached value is returned instead of fresh data. A build artifact is stale. A test is using old fixtures.

If your code seems to be doing the right thing but the behavior matches what used to be correct, suspect caching. Try clearing the cache, forcing a rebuild, or restarting.

Scope Confusion

A variable exists in one scope but you're trying to access it from another. A variable name is reused in a nested scope, shadowing the outer one. A closure captures a variable by reference and the value has changed by the time the closure runs.

When a variable has a value you didn't expect, check where it was defined and whether the scope allows what you're trying to do.

Part Six
Debugging AI-Generated Code
The new failure modes — AI code fails differently than bugs written by humans.

The New Failure Modes

AI coding tools have introduced a category of bugs that didn't exist in the same way before. They're important to understand because they fail differently than bugs written by humans.

Confident Fabrication

AI models will write plausible-looking code that calls functions, methods, or APIs that don't exist. They'll invent a function name that sounds right and fits the context. It compiles. It might even run. Then it crashes with a "method not found" error.

⚠️ The Pattern
An error saying a specific function, method, or import doesn't exist — but the code looks like it should. Check the actual documentation. Don't assume it exists just because the code looks reasonable.

Outdated Patterns

Models are trained on code from across time. They might use an API that was deprecated two years ago, a pattern that was replaced by a better one, or a library version that's no longer current. The code is syntactically correct and would have worked previously. It doesn't work now.

When you get errors that suggest an API or method exists but doesn't behave as expected, check the current version of the documentation.

Superficially Correct but Subtly Wrong

AI code often handles the happy path correctly and handles error cases incorrectly or not at all. The main flow works. But edge cases — empty inputs, large inputs, null values, network failures, concurrent access — produce wrong results or crashes.

📝 Systematic Check
Ask: what happens if the input is empty? If it's null? If it's the maximum expected size? If this external call fails? If this is called twice simultaneously? AI-generated code often doesn't ask these questions itself.

Copy-Paste Context Errors

AI generates code based on the context you provided. If you asked it to write a function for one context and then adapted that code for another, there may be constants, variables, or assumptions baked in that still refer to the original request. Field names, endpoint paths, hardcoded values.

When adapting AI-generated code, read every line for context dependencies. Don't assume it's context-neutral.

Inconsistency Across a Codebase

If you've generated multiple pieces of a system across different prompts, they might not fit together cleanly. The function in module A returns a dict; the function in module B that was supposed to consume it expects a list. Variable names don't match. Error handling conventions are inconsistent.

AI generates coherent local code but doesn't automatically ensure global coherence. Bugs at integration boundaries are common and worth looking for explicitly.

Using AI as a Debugging Partner

AI is genuinely useful for debugging, but in specific ways. Knowing how to use it well — and where it leads you astray — is its own skill.

Where AI Helps

  • Explaining errors you don't understand. Paste the error message and relevant code, ask what's happening and why.
  • Generating hypotheses. Describe what you're observing and ask for a list of things that could cause it.
  • Finding patterns you've been staring at too long. "Here's a function that should return X but is returning Y. Can you see what's wrong?"
  • Writing test cases. Once you understand the bug, AI can help generate tests that would have caught it.
  • Understanding unfamiliar code. AI can explain what someone else's code is doing and why it might be failing.

Where AI Misleads You

  • It will confidently suggest wrong fixes. It generates plausible-looking fixes based on the pattern of the problem. Always review as critically as you'd review code from a junior developer.
  • It can send you down rabbit holes. If your description is incomplete, AI will generate hypotheses that match your description but not the real cause.
  • It doesn't have full context. Showing it one file will produce answers that assume the problem is in that file. You often need to provide more context than feels natural.
  • It can lead you to symptoms instead of causes. It might correctly identify that a null value is causing the crash but not identify where the null is coming from.

How to Prompt for Debugging Help

Give the AI more context than you think is necessary: the exact error message, the full stack trace, the relevant code, and what you've already tried.

Be explicit about what you've already checked. "I've verified that X is not null at this point" prevents the AI from suggesting things you've already ruled out.

💡 Ask for Hypotheses
"What could cause this?" is often more useful than "Fix this" because it gives you a list to work through systematically rather than a single answer to trust.
Part Eight
The Stuck Protocol
What to do when you can't find it — a systematic process for getting unstuck.

What to Do When You Can't Find It

Everyone gets stuck. The difference between an experienced debugger and a novice isn't that the experienced one never gets stuck — it's that they have a protocol for getting unstuck.

Step 1: Write Down What You Know

Stop staring at the code. Open a text file and write:

  • What is the bug? What exactly is happening vs. what should happen?
  • When does it happen? What triggers it? When does it not happen?
  • What have you already checked and ruled out?
  • What do you believe the cause is, and why?

This forces precision. Often, the act of writing reveals either a gap in your reasoning or a clue you'd overlooked.

Step 2: Check Your Assumptions

Pick one thing you're treating as known and verify it explicitly. Print the value you're assuming is X. Check that the function you think is being called is actually being called. Verify that the data coming in is what you expect.

📝 The Key
Most stuck bugs involve at least one assumption that turns out to be wrong. The assumption you're least likely to question is usually the one to check.

Step 3: Explain It to Someone (or Something)

The rubber duck method: explain the bug out loud to an inanimate object. Or to a colleague, or to an AI. The act of explaining forces you to articulate things you've been holding implicitly, and those implicit things are often where the mistake is.

"So the user submits the form, and the data goes to the
endpoint, and the endpoint validates it, and... wait.
What does the validation actually return when it fails?"

Explaining reveals gaps. Follow the gaps.

Step 4: Take a Break

This is not laziness — it's neurologically real. Your brain continues working on problems when you step away, and the reduced focus often makes you notice things you were too close to see. A fifteen-minute walk has a non-trivial chance of solving a bug you've been stuck on for an hour.

Step 5: Change Your Approach Entirely

If you've been reading code, try writing tests. If you've been using print statements, try a debugger. If you've been debugging the complex system, try reproducing the bug in isolation. If you've been trying to understand why the bug happens, try reverting to a state where it didn't happen and working forward.

Sometimes the bug is invisible to your current approach. Changing approach changes what's visible.

Step 6: Search for the Error Exactly

Copy the exact error message and paste it into a search engine. Don't paraphrase. Include the exact text in quotes. Stack Overflow, GitHub issues, and language-specific forums contain a vast trove of people who've seen the same error.

Part Nine
Testing & Prevention
The best debugging is debugging you don't have to do.

Debugging Less

The best debugging is debugging you don't have to do. Tests don't eliminate bugs, but they catch them earlier, when they're cheaper to fix, and they prevent regressions — bugs that you fixed and then reintroduced later.

Write Tests for Bugs You Fix

Every time you fix a bug, write a test that would have caught it before the fix, and verifies it's fixed after. This serves two purposes: it proves the fix works, and it prevents the bug from silently coming back.

💡 The Habit
This habit — "fix a bug, add a test" — is how test suites grow toward the places that actually matter.

The Types of Tests

TypeWhat It TestsTradeoff
Unit testsSingle function or module in isolationFast and easy, but misses boundary bugs
Integration testsHow multiple components work togetherCatches more, but slower and more complex
End-to-end testsWhole system from the user's perspectiveClosest to reality, but slowest and most brittle

A healthy test suite has more unit tests than integration tests, and more integration tests than end-to-end tests. Invert this and you get a slow, fragile suite that's expensive to maintain.

Assertions

Assertions are checks baked into your code that verify your assumptions. They catch the gap between what you assumed and what's actually true.

assert user_id is not None, "user_id should be set before calling this function"
assert len(items) > 0, f"Expected at least one item, got {len(items)}"

They're cheap to write and make bugs fail loudly at the point where an assumption breaks — rather than silently producing wrong results several steps later.

Defensive Programming

The bug patterns in Part 5 suggest specific defensive habits:

☐

Check for null/undefined before accessing properties

☐

Verify array bounds before indexing

☐

Never trust the type of external data — validate or coerce

☐

Return copies of objects when you don't want mutation

☐

Check error return values — don't assume success

☐

Log at decision points for production traceability

You don't need to defend against everything everywhere. Focus on the boundaries: where data comes in from outside your code (user input, API responses, database results), and where different modules hand off to each other.

Part Ten
Debugging in the Browser
Frontend-specific tools and techniques — the browser DevTools are powerful and often underused.

The Browser DevTools

Frontend code has its own debugging environment: the browser's built-in developer tools. Every modern browser has DevTools accessible via right-click → Inspect, or F12.

The Tabs You'll Use Most

TabWhat It Shows
ConsoleJavaScript errors, console.log() outputs, interactive JS execution
NetworkEvery HTTP request: URL, method, status, headers, body. Essential for debugging API calls.
SourcesJavaScript debugger. Set breakpoints, step through code, inspect variables.
ElementsActual HTML and CSS as rendered. Useful for layout problems.
ApplicationCookies, local storage, session storage. Useful for auth and persistence bugs.

The Most Common Frontend Debugging Scenario

Something isn't working and you don't know why. Start in the Console tab: are there any errors? Address those first.

Then open the Network tab and reproduce the problem. Look at what API calls are being made. Click on the relevant one. Check the request body — is the data being sent what you expect? Check the response — what did the server return? Is the status code what you expected? For a deeper understanding of status codes, request/response anatomy, and API error handling, see the APIs field guide.

💡 Most Bugs
Most frontend bugs either appear as Console errors or reveal themselves in the Network tab as incorrect API behavior.

Debugging CSS

CSS failures are usually one of: the rule isn't applying (check specificity — a more specific rule is overriding), the rule is applying but not having the effect you expected (check your mental model of the property), or the element isn't where you think it is (check the box model in Elements).

In the Elements tab, clicking on an element shows all CSS rules applying to it on the right side, including which rules are being overridden (shown with strikethrough). This is usually faster than guessing.

Part Eleven
The Psychology of Debugging
Debugging is cognitively demanding and emotionally frustrating. Both sides matter.

The Human Side

Debugging is cognitively demanding and emotionally frustrating. Both the cognitive and emotional sides matter.

Frustration Degrades Performance

When you're frustrated, you narrow focus, skip steps, and start guessing more. This is the opposite of what debugging needs. Frustration leads to "I'll just try this" rather than "I'll test this hypothesis."

Recognizing that you're frustrated and doing something about it — taking a break, changing approach, asking for help — is a debugging skill. Not a soft one. A real one.

The Sunk Cost Trap

"I've been trying this approach for two hours, I must be close." Maybe. Or maybe the approach is wrong and you should abandon it. The time you've spent doesn't change the probability that your current approach will work.

⚠️ The Real Question
The question is never "how long have I been trying this?" The question is "is there evidence that this approach is leading somewhere?"

When to Ask for Help

There's no shame in asking for help, and there's no virtue in suffering alone for longer than necessary. But there's a skill to asking well.

Before asking: make sure you've reproduced the bug, can describe it precisely, and have already tried the obvious things. Then ask with the minimal reproducible example and what you've already tried. This respects the helper's time and forces you to articulate the problem clearly — which often resolves it before you even finish asking.

The Click

When you find the bug — when it all makes sense — there's a distinctive feeling. Satisfaction, and often a slightly sheepish recognition that the answer was there the whole time.

That feeling is the reward. It doesn't come from guessing — it comes from methodical investigation. You built toward it step by step. The method is what makes that click reliable rather than lucky.

Debugging Cheat Sheet

When You See an Error

  1. Read the full error message and stack trace
  2. Find the first line of the trace in your code
  3. Form a hypothesis: what would cause this?
  4. Test the hypothesis before changing anything

The Debugging Loop

  1. Reproduce reliably
  2. Isolate — binary search to the cause
  3. Understand — why is this happening?
  4. Fix — now change the code
  5. Verify — does the fix work?
  6. Write a test — so it doesn't come back

When You're Stuck

☐

Write down what you know and what you've tried

☐

Check one assumption you haven't verified

☐

Explain it out loud (rubber duck)

☐

Take a break

☐

Change your approach entirely

☐

Search the exact error message

AI-Generated Code: Specific Checks

☐

Do all functions/methods called actually exist?

☐

Are you using current API versions, not deprecated ones?

☐

Does error handling cover the likely failure cases?

☐

Are there context dependencies from the original prompt?

☐

Do the pieces fit together at integration boundaries?

Common Bug Patterns to Check First

PatternQuick Check
Off-by-oneIs the loop range right?
Null/undefinedDid this get set when you expected?
Wrong typeIs this actually the type you think?
MutationIs something being modified that shouldn't be?
Silent failureIs an error being caught and swallowed?
Stale cacheIs old data being returned?

Frontend Debugging

  1. Open DevTools Console — any errors?
  2. Open Network tab — what requests are being made, what did they return?
  3. Click into the relevant request — check request body and response
  4. For layout: Elements tab, look at applied CSS rules

What You Can Do Now

Next time you hit a bug, resist the urge to immediately start changing code. Instead:

  1. Reproduce it reliably. Make sure you can trigger it on demand.
  2. Read the error message completely, including the stack trace.
  3. Form a hypothesis about what's causing it before touching anything.
  4. Add one print statement or checkpoint to test whether the hypothesis is right.
  5. Follow the evidence.

That sequence — reproduce, read, hypothesize, test, follow — is the foundation. Once it becomes habit, debugging stops feeling like guessing and starts feeling like investigation.

The bugs don't get easier. Your ability to find them does.