Quick Answer

Read the error message properly, reproduce the bug reliably, then bisect until you find the smallest failing case. Verify assumptions by printing actual values rather than reasoning about what they should be.

Read the error. Actually read it.

The most common beginner behaviour is seeing a red wall of text, feeling a spike of stress, and immediately searching or changing something. The error usually contains the answer.

Three things to extract every time:

  • The exception type. TypeError, KeyError, NullPointerException — each names a category of mistake.
  • The message. Often extremely specific. "Index 5 out of bounds for length 3" tells you both the value and the limit.
  • The line, in your code. A traceback usually ends inside a library. Read upwards to the last line in a file you wrote — that is nearly always where the problem is.

Read the stack trace bottom to top and the picture assembles itself. Students routinely spend twenty minutes on an error whose message stated the cause exactly.

Reproduce it reliably first

A bug you cannot trigger on demand cannot be fixed with confidence — you can only stop seeing it and hope.

Work out the exact steps that produce it. Does it happen every time or occasionally? With any input or specific input? On a fresh start or only after other actions?

Intermittent bugs almost always mean one of a small set of causes: something time-dependent, something order-dependent, uninitialised state, or concurrency. That narrowing alone often identifies it — see process vs thread for the concurrency version.

Once reproducible, you also have your test for whether the fix worked. Without it, you cannot tell a fix from a coincidence.

Bisect: halve the search space

The core technique, and it works on any size of problem.

You know the input is right and the output is wrong. Somewhere in between it goes wrong. Check the middle. If the value is still correct there, the problem is in the second half; if not, it is in the first. Repeat.

Each check halves the search space, so even a thousand-line path takes about ten checks. Compare with reading top to bottom hoping to spot it.

The same idea applies at other scales. In time: git bisect finds the commit that introduced a regression by halving the history. In code: comment out half the feature and see if the symptom remains. In data: if one row of ten thousand breaks the import, test the first five thousand.

Check assumptions instead of reasoning about them

Every bug that takes hours involves something you were certain about that was false. "The function is definitely being called." "The list definitely has items." "The config is definitely loaded."

The fix is to stop reasoning and look:

print(f"user={user!r} type={type(user)}")
print(f"items={len(items)} first={items[0] if items else None}")

Print the actual value and its type. Use !r in Python — it shows quotes, so you can tell the string "5" from the number 5, and the empty string from None. That distinction alone resolves a large share of bugs.

Better still, use a debugger and set a breakpoint, which shows every variable at once without deciding in advance which to print — see VS Code setup and shortcuts.

Also verify the code is running at all. Editing the wrong file, a stale build, a cached asset or a server not restarted are all common enough that a temporary print at the top is a reasonable first check.

When you are properly stuck

  • Explain it out loud, in full. Rubber duck debugging works because articulating each step forces you to check the ones you were skipping. People routinely solve it mid-sentence.
  • Take a break. Twenty minutes away beats another hour of the same approach. This is not procrastination; the effect is real and well known.
  • Change one thing at a time. Changing five things and finding it works leaves you not knowing why — and the other four changes may have introduced new bugs.
  • Undo your debugging changes. Keep track of what you altered while investigating, or you will fix the bug and leave three new ones behind.
  • Search the exact error text, minus your own file paths and variable names.

When asking for help, include what you expected, what happened, the exact error, and what you already tried. That format gets useful answers and frequently solves the problem while you write it — which is the same mechanism as explaining out loud.

Frequently Asked Questions

Why does my code work sometimes and not others? Intermittent behaviour usually indicates time dependence, order dependence, uninitialised state, or concurrency. Narrowing to which of those it is usually identifies the bug quickly.
Should I use print statements or a debugger? A debugger shows every variable at a breakpoint without deciding in advance what to inspect, and conditional breakpoints stop on the exact iteration you care about. Prints remain useful for tracing flow over time.
How long should I struggle before asking for help? Long enough to have read the error, reproduced it and checked your assumptions — typically thirty to sixty minutes. Writing up the question properly often solves it before you send it.
What is rubber duck debugging? Explaining your code line by line to something that cannot help, out loud. It works because articulating each step forces you to verify the assumptions you were silently skipping.
The error points into a library. What do I do? Read the stack trace upwards to the last line in a file you wrote. The library is almost always reporting a problem caused by what you passed it, and that line is where to look.