Programming

How to debug

Debugging means shrinking "it is broken" step by step until you reach one line; the fix itself usually takes seconds, and what takes time is getting to that line. What makes the shrinking possible is neither a tool nor experience, but a reliable repro: until you can produce the failure whenever you want, every change you make is a guess.

  • Lesson 12 of 14
  • Beginner
  • Free, no signup

From "it is broken" to one line

  1. 1

    It is broken

    Whatever the user saw. Still a report, not yet a problem.

  2. 2

    What makes it happen again?

    Shortest path, the same data, a defined starting state. Without this band the rest mean nothing.

  3. 3

    Which layer?

    Browser, network, server, database. One network request or one log line closes this band in a minute.

  4. 4

    Which function?

    This is where halving belongs. Ten halvings take a thousand lines down to one.

  5. 5

    Which line?

    The line where your belief and the program part company. Usually not the line the error was printed on.

Now the fix takes seconds

A band only closes when you have evidence, not when you have a hunch. Jumping from the first band to the last is exactly what costs hours.

Last checked: Facts and tool names in this lesson are re-checked against their sources on this date.

What is debugging, and why does most of your time go to finding rather than fixing?

Debugging means closing the distance between "it is broken" and "this line is wrong". In most bugs a beginner meets, the fix is one word or one symbol. The whole job is reaching that word.

There is a sharper definition that makes everything simpler: a bug is a place where what you believe about the program differs from what the program actually does. A program never behaves "strangely" and has no moods; it does exactly what you wrote. So debugging means finding the point where your picture parted company with reality.

That definition has a practical consequence. If a bug is a gap between belief and reality, anything that puts the two side by side is progress and anything else is not. Printing a value is progress. Changing a line in the hope that it might fix things is not; that is gambling, and its worst property is that you learn nothing even when you win.

And one thing nobody tells beginners: your speed at debugging depends far less on how well you know the language than on how disciplined your method is. An experienced developer walks the same five stages, just discarding irrelevant guesses faster.

Why does everything start with making the failure repeatable?

A reliable repro means you can write down: "do this, and this happens." Until you have that sentence, nothing else means anything. Whatever you change afterwards, you will not know whether it is fixed or whether it simply did not happen this time.

A good repro has three things: the shortest path that reaches the failure, the exact data the failure happens with, and a defined starting state. "Sometimes it errors when placing an order" is not a repro. "With a freshly created user, an empty basket and an expired discount code, the submit button returns a 500" is a repro, and that sentence alone has done half the work.

Now a trap that has cost time on this very site and is written into our own engineering notes: your repro may not be testing what you think it is testing. When we delete a page cache file here, the web server keeps serving that deleted file for up to a minute, because it caches the result of the file lookup for 60 seconds. So if you open the page straight after clearing it, you see the old page and conclude that clearing did not work. It did work; you looked too early.

This is a specific case of a general mistake: between the thing you did and the thing you see, there is a layer you do not know about. The browser cache, the network cache, the code cache, an old copy of the file on another server. The first question to ask is not "why does it not work", it is "am I even running the thing I changed?" In PHP code the simplest way to prove it is to put a deliberately wrong print line in and see whether it shows up at all.

How do you actually read an error message?

Most beginners do not read the error message. They look at it, flinch at the red, and go straight to a search engine. Yet the message usually says exactly what you need, and nobody taught them the order to read it in.

An ordinary error has three parts. The type says what class of thing happened. The text says what happened to what. The location gives a file and a line number. Behind them there is usually a stack trace, the chain of functions that were called to get here.

Do not read the stack from the bottom up. The line closest to the error is at the top, since most languages list newest first. But what you are really looking for is not the closest line; it is the closest line you wrote yourself. If the first five entries belong to a library you did not write, the problem is usually not there; it is in the first line of your own code that called that library and handed it something wrong.

And the most important point in this section: the line number in an error is where the problem was noticed, not necessarily where it was created. If line 42 says a variable is empty, line 42 is fine; the place that should have filled it and did not is higher up. An everyday example: "calling a method on null" is almost never about the line it is printed on. It is about the line you expected to build that value, which returned nothing instead, and there may be ten function calls between the two.

What do you do when nothing errors at all?

The hardest bugs are the ones that do not error. The program runs, finishes, and the result is wrong or there is no result at all. The first thing to establish here is whether there really is no error, or whether there is one and you cannot see it. Those are very different, and in most cases the answer is the second.

Three usual places an error goes missing: a language setting that turns error display off on the server, a catch block that swallows the error and does nothing with it, and the wrong output channel. That third one is the least known and eats the most time.

A command line program has two separate output channels and an exit code. If you only read the first, a broken tool and an empty result look identical to you. A real example from this server, on the day this lesson was written: we ran the WordPress database query command and it produced zero bytes on standard output. Concluding "there are no rows" would have been wrong. The exit code was 1, and the error channel said it could not connect to the database at all.

So the rule of this section: an empty result is not evidence of absence. Before you trust an empty output, look at the exit code and read the error channel separately.

The same mistake has a simpler form that happens to everyone: your search tool finds nothing and you conclude the thing does not exist. On this very site, the robots tag on the home page is printed with single quotes. Search for it with double quotes and you get zero results and declare the tag missing. The tag is there. Your pattern was wrong, and the tool said exactly that: "what you asked for was not found." The rest you added yourself.

Does the program error, or does it quietly do the wrong thing?

It errors

Read the message in full

  • Type, text, file and line number
  • In the stack, the first line you wrote yourself
  • The error line is where it was noticed, not created
Silently

First prove there really is no error

  • Is error display off on the server?
  • Is a catch block swallowing it?
  • Did you check the exit code and the error channel separately?

The two branches need completely different methods. Most wasted time belongs to someone standing on the silent branch, hunting for an error message.

Halving: the shortest route to that one line

Once you have a repro, you have read the error, and you still do not know where it is, there is a method that almost always works and does not depend on how clever you are: halve it.

The logic is simple. If your program is a thousand lines and you do not know where the problem is, you have a thousand places to look. Disable half of it and if the failure is still there, five hundred places are gone. Ten halvings take a thousand down to one. Ten steps, for a problem that took hours of staring.

You will meet three practical forms of this in your own day. On code: comment out half the functions or return early. On data: cut the thousand-row input file to five hundred and see whether the failure survives, until you reach the smallest input that still fails; that input is half the answer by itself. On time: if it worked yesterday and not today, search between two versions rather than between a thousand lines. Git has a command for exactly this, git bisect, which does the halving for you and asks you only to say, after each step, whether that version was good or bad.

And one point that saves beginners: while halving, always keep the smallest example that still fails to one side. A ten-line file that produces the same error is infinitely more valuable than a thousand-line project that produces the same error, because there is nothing left in it to distract you. If you go on to ask someone for help, you send those ten lines and not the whole project.

One round of halving

  1. Switch half of it off

    Half the functions, half the input rows, or a range of versions. Which one does not matter; that it is half does.

    1
  2. Run the same repro again

    Exactly the same path and the same data. If the repro changes, this round tells you nothing.

    2
  3. Still broken? Throw the clean half away

    If it still fails the problem is in the half left on; if it works, in the half switched off. Either way half the space is gone.

    3
  4. Keep the smallest failing example

    Ten lines that reproduce the same error get you there and are also what you hand to whoever helps you.

    4

The loop only works while exactly one thing changes per round. Two changes at once turn it back into guessing.

Change one thing at a time, and write down what you tried

Once you are down to a small area, you will be tempted to change three things at once to be done sooner. If it works, you do not know which one fixed it. If it gets worse, you now have three variables instead of one. This is the one place where hurrying reliably slows you down.

The right method has a fixed shape. Form a hypothesis that can be proven wrong: not "I think it is the cache" but "if it is the cache, clearing it will change the result." Then pick the cheapest test that confirms or kills that hypothesis, change that one thing only, and look at the result. If the hypothesis was wrong, put the change back. Beginners skip that last part, and an hour later the code is full of unrelated edits that are themselves new bugs.

And a habit nobody believes in until they try it once: open a text file and write down everything you tried and what happened. Three words per line. It gives you three things: you stop testing the same thing twice, you do not start from zero when you come back tomorrow, and if you have to explain the problem to somebody, the explanation is already written.

Very often writing the explanation is what produces the answer. People even have a name for it: explaining the problem out loud to a neutral listener, even if that listener is a plastic toy on the desk. The reason is that explaining forces you to say your assumptions one at a time, and the bug is usually in the assumption that felt so obvious you never said it out loud.

Print or a debugger? And what about a live server?

You have two main tools, and arguing about which is better is pointless, because they do different jobs.

Printing means adding a line that shows the value of a variable. Its advantage is that it works everywhere, from a small script to a server you have no access to, and needs nothing installed. Its drawback is that each run gives you one snapshot, and if you put it in the wrong place you have to run again. For most everyday bugs it is enough, and whoever says professionals do not print has not watched professionals up close.

A debugger stops the program in the middle and lets you see every variable at once and step forward line by line. It earns its keep when you do not know where to print in the first place, or when the path through the code has so many branches that you cannot tell which one was taken. Learning it costs half a day and you get that half day back quickly.

On a live server neither works in its usual form. You cannot attach a debugger and you must not print something a visitor will see. There you have one thing: the log. And here is a common mistake worth telling once: writing to the log does not mean being seen. The server may have error levels set so your message is never recorded, or the log file may be somewhere you would not think of. Before you go hunting a bug, write one meaningless line to the log and find it. Until you have seen that line, not seeing the other messages proves nothing.

Three things that must never go into a log, while we are here: passwords, tokens, and personal user data. A log is a file several people read and it stays around for a long time.

The fast path, with AI

The fast path is not "paste the error and ask why". That gets answers which look right and have nothing to do with your program, because the model does not have the three things it cannot guess. What is actually fast is this: gather three pieces of evidence, and ask the model not for a fix but for <strong>a ranked list of hypotheses, each with the cheapest test that would disprove it</strong>. You run one of those tests yourself, and in one round half the list is gone. This is judgment work, so it wants a frontier model rather than a fast one; our current pick among coding models is in <a class="text-link" href="/ai/code/">the best AI for coding</a> and is kept up to date there.

  1. Gather the three pieces of evidence: the exact repro, the full error text with its stack, and a list of what changed since it last worked.
  2. Say explicitly that you do not want code. At this stage you want hypotheses only, otherwise the model writes a patch instead of thinking.
  3. Run the cheapest test on the list yourself and hand back the raw result, with no interpretation. Your interpretation is exactly what makes the model agree with you.
  4. Once one hypothesis survives, only then ask for the fix, and ask for the smallest possible change. Then run the original repro yourself.

Copy-ready recipe

I have a bug and I do not want code. I want hypotheses only.

EVIDENCE 1 - exact repro:
{I do this: ...} {this happens: ...} {every time / intermittently}

EVIDENCE 2 - full error text (if there is one):
{paste here, with the stack trace, do not summarise}
{if there is no error, write: no error. Observed output: ... Expected output: ...}

EVIDENCE 3 - what changed since it last worked:
{code / library version / server config / data / nothing that I know of}

ENVIRONMENT: {language and version} on {OS or server}

WHAT I WANT:
1. At most 5 hypotheses for the cause, ordered most to least likely.
2. For each, the cheapest test that confirms or kills it, as one command or one specific check.
3. For each, what else I should also be seeing if it were true. If I do not see that, the hypothesis dies.
4. Separately, what you do not know that would change the ordering if you did.

Write no code. I will ask for the fix in the next message.

Before you trust the output: The hypothesis list is guesses ordered by resemblance to what the model has seen, not by your program. Run the tests yourself and never make more than one change per round. And three things do not belong in the message: passwords and tokens, logs containing users personal data, and a client codebase without their permission.

AI in this kind of work

In debugging, models genuinely do one thing better than you: reading a stack trace from a library you have never seen and telling you what that message usually means. There is a second thing they do well and almost nobody uses: producing the list of hypotheses, because making a list is exactly what your tired mind stops doing after two hours. Our position is to cast the model as a hypothesis generator and not a repairman: deciding which hypothesis to test, and what the test showed, stays yours.

Tools that actually help

  • Claude Code For debugging its main advantage is that it sees the repository, so instead of asking you where that function is called it goes and finds out. The tool installs free but does not run without a Claude subscription or an Anthropic Console account, and Iran is not on Anthropic supported-countries list.
  • GitHub Copilot For this topic it has one real advantage: it lives in the editor, so when the failing line is already open you do not paste anything anywhere. Its access story is unusual and worth knowing: GitHub own trade-controls page says it holds a US Treasury licence covering its cloud services for developers resident in Iran, free and paid. We quote that sentence and claim nothing beyond it: paying from Iran is a separate matter and we have not tested it. The free plan runs to two thousand completions a month.
  • Claude It suits exactly what this section recommends: hand it the full error and ask for a hypothesis list, no code. Iran is on neither of Anthropic two supported-countries lists; we read that on Anthropic own page rather than measuring it.
  • Gemini Good for when the error comes from a large, heavily documented library or service. Google own page says the Gemini web app works in more than two hundred and thirty countries and territories, and Iran is not on that list.

Where it backfires

The risk specific to debugging is that the model agrees with you. Write "I think it is the cache" and it will produce reasons why it is the cache, and those reasons will sound sensible. You went looking for a second opinion and got a loudspeaker. That is exactly why the recipe above says to withhold your interpretation and hand back only the raw test result.
The second risk has a number and a source. In the 2025 Stack Overflow developer survey, the biggest frustration developers reported with AI tools was "AI solutions that are almost right, but not quite" at 66 percent, and the second was "debugging AI-generated code is more time-consuming" at 45.2 percent. Which means the very tool you reach for to fix bugs faster can, if you let it write code, add a fresh share to your debugging work. Anthropic also calls this class of unearned confidence hallucination in its own documentation and explains how to reduce it. For how each of these tools can actually be paid for from Iran, see the buying guide.

Sources: Stack Overflow 2025 Developer Survey: AI Anthropic: reduce hallucinations Anthropic: supported countries GitHub and Trade Controls GitHub Copilot plans Google: where the Gemini web app is available

Where this advice stops

Everything in this lesson stands on one assumption: the failure can be reproduced. For bugs that only appear under heavy load, or only when two things run at once, or once every few days, this method does not work and a different one is needed, whose name is logging and waiting, and which deserves its own lesson. Three more things are deliberately left out: concurrency bugs, diagnosing something whose source you do not have, and profiling tools, which are for slowness rather than for breakage. And one last honesty: narrowing does not always end at a line. Sometimes it ends at a design decision made three months ago, and that is no longer a bug.

From our own work

The "an empty result is not evidence of absence" example is not recalled from memory; it was run on this server on the day this lesson was written. We ran the WordPress wp db query command with a simple count over the posts table: standard output produced zero bytes, the exit code was 1, and the only thing that had actually happened was written on the error channel: ERROR 2002 (HY000): Can't connect to local server through socket '/run/mysqld/mysqld.sock'. The tool had not even reached the database. A script reading standard output alone sees exactly what a search with no matches produces, and has no way to tell the two apart. The same day we took the second example off this site home page: the robots tag is printed with single quotes, so a search for name="robots" returns zero results while the tag is sitting right there.

Real follow-up questions

How do I tell whether the problem is my code or the server?

Run the smallest possible program in the same place: a file that only prints one sentence. If even that fails, the problem is not your code. If it works, the server is fine and you grow that smallest example step by step until it breaks; that step is the answer.

Should I learn a debugger, or is printing enough?

For the first months printing is enough and nobody will judge you for it. The sign that it is time is catching yourself adding print after print and rerunning each time; that pattern means you do not need separate snapshots, you need to see everything at once. Learning it is half a day.

My code works on my machine but not on the server. Where do I start?

From the list of differences, not from the code. Language version, library versions, environment variables, file permissions, timezone and error display settings. The answer is almost always one of those six, and comparing them pair by pair is faster than rereading code you already know is correct.