DebuggingGENERALSTAGE-SPECIFICILLUSTRATIVE

Do Not Randomly Change Things

Error → change some code → restart → change something else is the most common debugging method and the worst. Instead ask what changed, what evidence exists, and which layer owns the symptom — three questions that cost less than one restart.

The moveWorked exampleNext questions▶ Debugging Lab

The situation, the reflex, and why it stalls

Every lesson starts where being stuck starts: someone has a problem, and the first move that comes to mind feels like progress.

The question

You have been editing, restarting and re-trying for an hour and the error is still there. What should you have done instead, and how do you start doing it now?

The situation

Payment failed. You changed the timeout, restarted, still failing. You commented out the validation, restarted, still failing. You switched the provider SDK version, restarted, now a different error. You are not sure which of your changes did that, and you cannot remember what the original error said.

The reflex

Try something. The error is right there and the edit-restart cycle is quick, so each attempt feels like it might be the one. It is a reflex because it sometimes works, and the times it works are memorable.

Why it stalls

Each change is an experiment with no hypothesis, so a negative result teaches nothing: "it still fails after raising the timeout" only rules out the timeout if the timeout was ever a candidate for a reason.

What the reflex produces — and fails to produce
  • Each change is an experiment with no hypothesis, so a negative result teaches nothing: "it still fails after raising the timeout" only rules out the timeout if the timeout was ever a candidate for a reason.
  • Changes accumulate. After the fourth edit the system is in a state nobody chose, the original symptom may be masked by a new one, and the question has silently become "what did I do?" instead of "what is wrong?".
  • When a change appears to work, it is kept without understanding. The bug was intermittent, or the restart cleared a cache, and it returns next week with the "fix" now in the way of finding it.
  • The method is "change things until it works", and its honest description is that it searches the space of edits at random, one per restart, with no memory. That space is enormous and the search has no way to know it is getting closer.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

Precisely enough to apply it to a problem you have never seen — not a slogan.

  • Stop and ask the three cheap questions before any further edit. What changed — in the code, the config, the data, the dependencies, the environment — between the last time this worked and now? What evidence already exists — logs, network responses, database rows, dashboards — that I have not read? Which layer owns the symptom — is the message produced by the UI, by our backend, by the provider, or by the database?
  • Each answer removes candidates. A known change points at a diff; existing evidence usually names a layer; the owning layer says where the next observation should be made. Only when all three have been asked does a code change become an experiment rather than a guess.
  • If you have already made random changes, revert them all first. You cannot debug a system whose state you do not know, and the original symptom is the one worth understanding.
  • When you do change something, change one thing, predict what it will do, and keep the observation. A change with a prediction is an experiment; the same change without one is mutation (Predict Before You Look).

Mutation against experiment

The same edit can be a mutation or an experiment. The difference is not the edit; it is whether a prediction preceded it and whether the result updates a hypothesis. The pair below is the same change to the same code made two ways.

Raising the timeout
Mutation
Raise the provider timeout from its current value to something larger, restart, click Pay, see "Payment failed", shrug, comment out the validation next.
Experiment
Hypothesis: the provider is slow and we time out. Prediction: if so, the network tab shows the request taking about the timeout length before failing, and the backend log says timeout. Observation: the request fails in a fraction of a second with a 500. Hypothesis dead; the timeout is never touched.

The experiment ruled out the timeout without changing it, and the observation it made — a fast 500 — also points at the next hypothesis. The mutation changed the system, learned nothing, and left the larger timeout in place to confuse the next person.

What random changes do to a system

Each kind of random change has its own way of making the bug harder to find. The table names the common ones; the response column is always some form of "revert, then ask the three questions", because there is no random change whose damage is undone by a further random change.

Random changes and their aftermath
TriggerSymptomCauseResponse
Restart the service "to see"The error goes away for a while and returnsThe restart cleared state — a pool, a cache, a queue — that a real defect was fillingTreat the disappearance as evidence about state, not as a fix; read the log from just before the restart.
Comment out the failing checkA different error further along, or silent bad dataThe check was correct and the input was wrong; removing it moved the failure past the point where it was detectableRestore the check; the input is the lead (Reading the Error Message).
Bump or pin a dependencyA new, unrelated errorTwo changes now overlap — the original bug and an API change in the dependencyRevert the bump; if the dependency is suspected, test that one hypothesis on its own.
Copy a fix from a search resultNo change, or a change that cannot be explainedThe fix was for a different cause with the same messageUse the search result as a hypothesis to test, not as a patch to apply (Search as a Skill).
Several changes at once, then it worksNobody knows which change matteredNo isolation; the "fix" may be any one of them or the restartRevert all, reapply one at a time with a prediction each.

The order that replaces guessing

The three questions are cheap and they compound: an answer to "what changed?" usually tells you where the evidence is, and the evidence usually names the layer. The order below is the default; the alternative applies when the system is opaque and the first question is about restoring visibility.

Before the next edit
  1. 1
    Revert to a known state and record the original error verbatim

    because Every later observation is meaningless if the system is in a state you drifted into.

  2. 2
    What changed? — read the diff, the deploy log, the lockfile, the config, recent data changes

    because Most bugs in working systems were introduced by a change, and the change is recorded somewhere.

  3. 3
    What evidence exists? — network tab, backend log for the request id, provider dashboard, order row

    because The running system already knows more than you do; reading it costs seconds.

  4. 4
    Which layer owns the symptom? — the one that produced the non-OK response, not the one that displayed it

    because The next observation should be made in the layer that failed, and the frontend rarely is.

  5. 5
    One hypothesis, one prediction, one change or observation

    because A change with a prediction is an experiment; its result updates what you know either way.

a different valid order Visibility-first: in a system with no request logging and no correlation ids, start by adding a log line at the boundary that carries the request id and the provider response, because until it exists the "what evidence?" question has no answer. You would choose this when the evidence step comes up empty, and it is a good moment to notice the system was built without Debuggability by Design.

How to do it

Most important first.

  • Revert to a known state. git stash or a fresh checkout; note the exact original error text before it is lost.
  • Answer "what changed?" from records, not memory: the deploy log, the diff since the last known-good commit, the dependency lockfile, the config, the data. The cause is very often in that list (Binary Search Over the System).
  • Read the evidence that already exists before generating more: the full error and stack, the request in the network tab, the backend log for that request id, the provider's dashboard, the state of the order row (Logs Are Evidence, Not Thinking).
  • Name the layer that produced the message. If the frontend shows "Payment failed" for any non-OK response, the frontend owns the wording and nothing else; the symptom actually lives wherever the non-OK response came from (Who Owns This State?).
  • Only then form a hypothesis and choose the single change or observation that tests it. Write what you expect to see before restarting (Prediction Before Execution).

Worked on a concrete problem

The move has to produce something. This is what it produced.

  • The hour of edits, reverted. The original error, recovered from the terminal scrollback: 500 Internal Server Error from /checkout/pay, not a provider decline. What changed: a dependency bump yesterday moved the provider SDK a major version. Evidence: the backend log shows charge is not a function. Layer: our backend, at the SDK boundary. Hypothesis: the SDK's API changed shape. One observation — the SDK changelog — confirms it. One edit fixes it, and the edit is explained.
  • Contrast the random path on the same bug: raising the timeout could never have helped (a 500 in a fraction of a second is not a timeout); commenting out validation could never have helped (the request reached the handler); switching SDK versions did produce a different error and was, by accident, near the cause — but without knowing why, the engineer switched back.
  • A subtler case: the change that "fixed" it. Restarting the backend made "Payment failed" go away for an afternoon. The three questions: what changed — nothing in code; evidence — the log before the restart shows connection pool exhausted; layer — the database client. The restart cleared the pool. The real cause is a leaked connection in the refund path, and it is still there (Connection Pool Saturation: Waiting in Front of an Idle Database).

How you know it worked

What now exists that did not before, and what question you can now ask.

  • You can state the original symptom exactly, and the system is in a state you chose.
  • You have a list of what changed, taken from records, and each item is either ruled out or a candidate.
  • Every edit you make now comes with a sentence beginning "if this is the cause, then after the change I should see…".
  • When something works, you can say why — and if you cannot, you keep looking instead of committing.

The questions you can now ask

The field this whole domain exists for. After this lesson, these are the questions to put to an unfamiliar problem.

Next questions
  • ?What changed between the last time this worked and now — in code, config, data, dependencies or environment?
  • ?What evidence already exists that I have not read?
  • ?Which layer produced this message, and is that the same layer where the failure happened?
  • ?If I make this change and I am right, what will I see — and what will I see if I am wrong?
  • ?Is the system currently in a state I chose, or one I drifted into?

What can go wrong

How the move itself fails
  • The three questions are asked once, superficially, and then the random edits resume. "What changed?" answered with "nothing" from memory, when the lockfile says otherwise.
  • Refusing ever to try a change. When the evidence is exhausted and two hypotheses remain, a change that distinguishes them is the right experiment. The move forbids changes without hypotheses, not changes.
  • Reverting is skipped because "I remember what I did". Half an hour later a changed timeout is still in place and is quietly hiding a second symptom.
  • The "layer" question is answered by blame rather than evidence — "it's always the provider" — which is a random change wearing a hypothesis's clothes.
What the move costs
  • Reverting an hour of changes throws away any accidental progress they contained. Occasionally one of the random edits was near the cause; the move loses that and gets back a system it understands.
  • Asking "what changed?" properly means reading diffs and logs, which is slower per attempt than an edit-and-restart — and faster per bug.
  • On a throwaway prototype where nothing is at stake and restarts are instant, poking at it is sometimes the fastest way to learn what a library does. The move is about bugs, not exploration.
Misreads
  • "So every debugging session needs a written plan." No — it needs a hypothesis before each change. For a bug whose cause is visible in the first log line, the plan is one sentence and the change follows immediately.
  • "Trial and error is always wrong." Trial with a prediction is an experiment and is exactly right. Error without a prediction is the problem; the word doing the work is *randomly*.
  • "The fix worked, so I must have understood it." A symptom disappearing is one observation. If you cannot say why the change removed it, you have a correlation and an unexplained system (Correlation Is Not the Root Cause).

Where this applies

Problem-solving advice is stated as universal far more often than it is. These labels say what each method is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.

  • GENERALApplies to any system where an edit-restart cycle exists; the temptation scales with how fast the cycle is, which is why it is worst in hot-reloading frontends and REPLs.
  • STAGE-SPECIFICIn production, random changes are also dangerous, not merely wasteful — a deploy to "try something" is a change to a live system with customers on it, and the discipline is enforced by process. On a local prototype the only cost is time and understanding.
  • ILLUSTRATIVEThe SDK major-version bump, the leaked refund connection and the hour of edits are invented to show the pattern, not taken from any real incident.

Where the depth lives

This domain asks the question and hands the answer off by name.

Further
  • The manifesto's review practice at /manifesto/review — "explain why the change fixes it before merging" — is this lesson applied at the pull request.