Skip to main content

Debugging Is a State Problem, Not a Code Problem

· 9 min read
Elisha Sterngold

A stack trace, the source code, and the runtime state that connects them

Debugging Is a State Problem, Not a Code Problem​

Show a senior engineer the exact function that threw in production. Give them the stack trace, the file, the line. Watch what happens.

More often than not, they read it, frown, and say some version of: this looks fine.

Because it is fine. That is the part nobody warns you about. The code behind last night's crash or error was almost certainly correct — for every input its author imagined. The bug is not sitting in the source, waiting to be spotted by a careful enough reader. If it were that visible, code review would have caught it and we would all be out of a job.

The bug is somewhere else entirely. And once you see where, a lot of confused arguments about AI and debugging suddenly resolve.

The Function Is Innocent​

Here is the uncomfortable reframe. Most production bugs are not flaws in the logic. They are collisions — between logic that is locally correct and a runtime state its author never pictured.

The array that was "always" non-empty, until the one code path that empties it first. The user object that is "always" populated by the time this runs, until a race lets it through half-built. The config value that is "always" a number, until a migration writes it as a string. In each case the function is doing exactly what it was written to do. It was just written against a reality that turned out to be narrower than the real one.

This is why "I can't reproduce it" is the most hated sentence in engineering. You have the code. You are staring at the code. The code is right there. And it stubbornly refuses to fail, because the thing that makes it fail is not in the code at all. It is the specific, unrepeatable state the code was in when the world handed it something it did not expect.

Debugging, at its core, is not the act of finding the wrong line. It is the act of reconstructing the state that made a right line go wrong. The line is usually the easy part.

Three Layers, and Where the Answer Actually Lives​

When a production error lands, you have — at most — three sources of truth about it. It is worth being precise about what each one can and cannot tell you, because teams routinely stop after the first two and then wonder why the fix is a guess.

The stack trace tells you where it surfaced. That is genuinely useful and genuinely limited. "Surfaced" is not "originated" — the throw is very often downstream of the real cause. A null blows up three frames deep in a formatting helper; the helper is blameless; the null was born in a caller that returned early under a condition the trace does not mention. The stack trace is a pin on a map. It is not the story of how you got to that spot.

The source code tells you what the logic is. This is the layer everyone reaches for, because it is the one that is always available and always feels like progress. But it is the obvious layer. It shows you the machine, not the failure. You can read the machine top to bottom and, as we just established, conclude — correctly — that the machine is fine.

The runtime state — the sequence of logs, events, and values leading up to the moment it broke — tells you the input reality that made correct code fail. This is the layer that actually contains the answer. It is also the one teams have the least of, because it is the hardest to capture. Code is static and sits in a repo forever. State is a fleeting thing that existed for a few milliseconds in production and, unless you deliberately recorded it, is gone.

Notice the asymmetry. The layer that holds the answer is the layer we invest in least. We version-control the machine and throw away the run.

Under-Grounded, Not Unreliable​

Now bring AI into it, because this is where the reframe pays off.

There is a common line that "AI debugging is unreliable." You ask a model to fix a production error, it hands you a confident, well-structured, completely wrong fix, and you conclude the model is not up to the job.

I do not think that is what is happening. I think the model is doing something entirely reasonable given what it was handed.

Give an AI only the stack trace and the source, and ask it why the code failed. It will do the exact thing the senior engineer did at the top of this post — read the machine, find it locally correct — except it cannot stop there, because you asked it for an answer. So it produces the most plausible story consistent with the code. That story is a guess. A fluent, structured, confident guess, because that is what these models are good at. But the state that would have disproven the guess was never in the prompt.

This is the same failure mode as asking any model a factual question it has no source for. It does not say "I lack the data." It reasons forward from what it has and produces something that sounds like an answer. The problem is not the reasoning. The problem is that the fix depends on facts that were never on the table.

So the right word is not unreliable. It is under-grounded. The model was asked to reconstruct a runtime state it was never shown. Of course the answer is a guess. A human handed the same two layers and no way to reproduce would guess too.

Retrieval Isn't Just for Chatbots​

The AI world already has a name for the fix, and it did not come from debugging. It came from chatbots.

When you build a support bot that has to answer "what is our refund policy," you do not rely on whatever the model absorbed in training. You retrieve the actual policy document and put it in front of the model, and only then let it answer. The industry calls this grounding — anchoring the model's reasoning to specific, real facts rather than its priors. It is the difference between a bot that makes up your refund window and one that reads it off the page.

Debugging is the same problem wearing different clothes. The error is the question. The fix is the answer. And the answer is only trustworthy if you ground it in the real facts of the case — which, as we have now said three ways, means both the code and the run.

The code half is the easy, obvious retrieval: pull the source at the offending lines, ideally from the exact branch that is actually in production, not whatever main looks like today. The run half is the one that matters and the one everyone skips: retrieve the log sequence and the session state that led up to the crash or error, so the model can see the input reality instead of inventing one.

Ground a model in both and the character of the answer changes. It is no longer reasoning forward from a machine that looks fine. It is reasoning over the actual collision — this code, meeting this state, producing this failure. That is not a smarter model. It is the same model, finally holding the facts the fix depended on.

The Part You Have to Build​

None of this works if the run is gone.

That is the quiet catch in the whole argument. You can retrieve the code any time — it is versioned, it is durable, it is sitting in Git. But the state that actually caused the bug existed for a moment in production and then evaporated, unless something was there to record it. The reason debugging feels like archaeology is that we treat the run as disposable and the code as sacred, when the run is where the answer lives.

So the practical takeaway is almost boring: capture the run. Log richly enough, and with enough sequence and session context, that when something breaks you can reconstruct the state it broke in — not just the line it broke on. Everything else, human or AI, gets easier downstream of that. The engineer stops guessing. The model stops guessing. The two of them can look at the same reconstructed reality and reason about it together, which is the only version of "AI debugging" I have ever seen actually work.

This is exactly what Shipbook's AI error analysis, Loglytics, does today. You connect your GitHub repository once, and from then on, when Loglytics analyzes an error, it grounds itself in both halves of the collision: it reads the real source behind the trace straight from your repo, and it pairs that with the session's log history leading up to the failure. Code and run, in front of the model together, before it says a word about the fix. That is the whole feature, and it exists precisely because a fix grounded in only one half is a guess. But the tool is not the point. The point is older than any of our products, and it will outlive them: the bug was never really in the code. It was in the state. Debug the state, and the code mostly explains itself.

The next time an AI hands you a confident fix that turns out to be wrong, do not ask whether the model is good enough. Ask what you showed it. Almost always, you showed it the machine and hid the run — and then blamed it for guessing at the part you kept to yourself.