Every Bug Is a Story About a Wrong Assumption
Why I started a blog about software failure instead of software success, and what a debugging writeup should actually contain.
There is a moment in every hard debugging session that I think of as the hinge.
It arrives after the obvious checks have failed. You restarted the service. You cleared the cache. You read the stack trace three times and it keeps pointing at a line of code that is obviously fine. Then something shifts, and you realise the problem was never in the code you were staring at. The problem was in something you believed so completely that you never thought to check it.
Everything before the hinge is thrashing. Everything after it is cleanup.
I have been building software professionally since 2019, first keeping other people's websites online at a shared hosting company in Surat, then four years building Shopify themes at Debutify, and now running Krova Cloud. I have collected a lot of these moments. What strikes me is how little writing exists about them. We have an enormous amount of material about how to build things: tutorials, getting started guides, architecture posts, framework comparisons. We have far less honest writing about how things break, and almost nothing about the process by which a person works out how they broke.
This blog is my attempt to fix that in a small way.
The asymmetry in technical writing
Think about what happens when you search for something you do not know how to do. Say you want to add server side rendering to an app. You will find hundreds of articles, several of which are the same article rewritten. Supply is abundant because the work is legible: make the thing work once, write down the steps, publish.
Now search for an error message. Not a famous one, a specific one, with a version number, in a slightly unusual context. What you find is different in character. A GitHub issue with fourteen "same here" comments and no resolution. A Stack Overflow answer from 2019 that no longer applies. A post clearly generated from the error string itself, offering the five generic suggestions anybody would have tried before searching.
The asymmetry exists because writing about failure is harder. To write a tutorial you need to make something work once. To write usefully about a failure you need to have actually experienced it, reproduced it, understood the mechanism underneath, and resisted the urge to stop the moment the symptom disappeared.
That last part deserves attention. Most bugs are not fixed. They are dispelled. Something changes, the symptom goes away, and the engineer moves on with a vague feeling that it was probably a caching thing. The understanding never arrives. Because the understanding never arrives, nothing gets written down, and the next person starts from zero.
What a wrong assumption actually looks like
I want to be concrete, because "check your assumptions" is the kind of advice that is true and useless in equal measure. Everyone agrees with it. Nobody knows what to do with it at two in the morning.
Here is a more useful framing. At any moment you are holding a mental model made of a few dozen beliefs. Most are so basic you have never said them out loud:
- The code running is the code I wrote
- The config in production matches the config in the repository
- This function is called once per request
- The clock on this machine is correct
- The string I am comparing has no trailing whitespace
- The library does what its name suggests
- This test failure is caused by this test
- The network either delivers the message or reports an error
Every one of these is false somewhere. A bug that resists you for hours is nearly always a bug where one of them is false in your specific case and you have not yet noticed which, because you are not looking at it. You are not looking at it precisely because it is load bearing. You cannot reason about the system at all without assuming most of these hold, so your attention slides straight over them.
The skill of debugging is not really about tools. It is developing a sense for which of your invisible beliefs is most likely to be the false one, then finding the cheapest possible experiment to test it. Tools matter, and I will write a great deal about them here, but they serve that.
The structure of a useful debugging writeup
If I am going to complain about the state of writing on failure, I should say what I think the good version looks like. This is the shape I hold myself to.
State the symptom precisely. Not "the app was slow" but "p99 latency on one endpoint went from 40ms to 900ms at 14:20 UTC while p50 was unchanged." The precision matters because the shape of the symptom is your primary evidence. A p99 change with a flat p50 tells you something very different from both moving together.
Include the false leads. This is the part that always gets edited out and it is the most valuable part. A writeup that jumps from symptom to root cause teaches the answer to one problem. A writeup that shows three hypotheses that were wrong, and why each was reasonable, teaches you how to think. When I read Julia Evans or Dan Luu, or a company incident report written honestly, the wrong turns are what I actually learn from.
Explain the mechanism. Not "adding an index fixed it" but why the planner was not using the existing index, what row count it estimated, and why the estimate was wrong. If a reader cannot predict when the same problem will recur, the post has not taught them anything durable.
State versions. Software behaviour is not timeless. "Node 22.14, Postgres 16.3, running in a container on ARM64" is not pedantry. It is the difference between a post that helps and a post that wastes an hour.
Say what would have caught it. Every bug is a gap in a feedback loop. Sometimes the answer is a type, a lint rule, an integration test, a metric, an assertion. Sometimes the honest answer is that nothing reasonable would have caught it, which is worth saying too.
Why this feels urgent now
A large share of code is now written by machines. I am not going to argue here about whether that is good. I use these tools daily and I think they are remarkable. But there is a structural consequence I do not see discussed enough: the bottleneck has moved.
For most of this field's history the expensive step was producing code. That is no longer reliably true. The expensive step is now verifying code, establishing that a change does what it claims, does not do anything it does not claim, and will keep holding when the system around it shifts. Generation got cheap. Verification did not.
Which means debugging is not a declining skill. It is becoming the central one. The engineer who can read a flame graph, reason about a race condition, or look at a plausible 900 line diff and spot the one place it silently swallows an error is worth more now than five years ago, not less.
And the failure modes are new. Code that is locally plausible and globally wrong. Functions that handle every case in the prompt and none of the cases in production. Tests generated from the implementation, which therefore assert that the bug is correct behaviour. That last one is genuinely dangerous and I will come back to it.
What to expect here
Roughly five kinds of post.
Error autopsies. One specific error message, reproduced from scratch, explained mechanically, fixed, then a ranked list of everything else that produces the same message. If you got here from a search engine with an error in your clipboard, these are for you, and the fix goes at the top. I am not going to make you scroll past my life story.
Bug hunts. Long form narratives of a single investigation, false leads included.
Tooling deep dives. How debuggers, profilers, and tracers work underneath. It is much easier to use a tool well when you know what it is doing to your process.
Infrastructure and ecommerce. I spent four years building Shopify themes at Debutify, finishing as CDO, so a good chunk of my debugging has happened in Liquid, in theme performance work, and in the strange interactions between merchant apps. I now run Krova Cloud full time, building isolated microVMs on our own hypervisor, which has moved my debugging down the stack into kernels, networking, and process isolation. Both of those show up here.
Opinions, occasionally. I think print debugging is underrated and I think most error messages are badly designed user interfaces. I intend to argue both properly.
Everything here is something I actually hit. If I have not reproduced it on my own machine, it does not get published. That is a slower way to run a blog and I think it is the only way to run one worth reading.
Next post is an error autopsy. Somebody has to go first.