How I find bugs faster than I used to — a beginner’s debugging order that actually works
Quinn Reed
August 28, 2026
I used to debug like I was trying to scare the bug away. Restart the server. Add five console.logs with names like here, here2, and wtf. Change two things at once. Ask Slack if “anyone else seeing this.” Paste the stack into a model before I had reproduced it twice. Sometimes I got lucky. Most of the time I got a longer evening and a fix I could not explain the next morning.
I am faster now, and it is not because I got smarter. It is because I stopped starting in the middle. I run the same order almost every time. The order is boring. Boring is why it works. If you are early in this job, the order will save you more hours than another course about “advanced breakpoints.”
This is the order I actually use. It is not a personality. It is a sequence I refuse to skip when I am tired.
1. Reproduce it, or you do not have a bug yet
The first question is not “why.” The first question is “can I make it happen again on purpose.” If the answer is no, you have a story, a screenshot, and a feeling. You do not have a bug you can fix.
I write down the smallest path that fails. Which URL. Which user. Which payload. Which browser. Which branch. I do it once from a clean state — new incognito, new session, seed data I understand. If it only fails on Tuesday after lunch, that is a clue, not a reason to start editing code.
I used to skip this because reproduction felt like delay. Then I spent a morning “fixing” a null that only existed because I had stale state in the REPL. The production error was a timeout. I had been debugging my own leftovers.
If you cannot reproduce it, your job changes. You gather: logs, request ids, the exact time, the exact user id, the deploy that was live. You do not start guessing in the function you “have a feeling about.” Guessing without a reproduction is how you ship a cleanup that does not touch the failure and then tell standup it is “hard to reproduce.”
2. Read the whole error. Then read the line above it.
I still watch people, including me on a bad day, read the first red word and start moving. TypeError. undefined. ECONNREFUSED. That is not reading. That is flinching.
Read the message. Read the type. Read the file and the line. Open that line. Read the line above it. Read the frame under it — the one that is your code, not the framework’s internals. If the top of the stack is node_modules, keep walking until you hit a path you own. The bug is usually in the last function you wrote that handed garbage downward, not in Express, Fastify, or Django.
A habit that paid for itself: copy the error into a note, then write one sentence in my own words. “The webhook handler assumed customer.id and Stripe sent a deleted customer.” If I cannot write that sentence, I do not understand the error yet. I am not allowed to “try something.”
Status codes lie in a specific way. A 500 is not a cause. A 404 is not always “missing.” I have chased a 404 that was a wrong base URL in env, and a 500 that was a swallowed 401 from an upstream. Read the body. Read the upstream. The red badge in the network tab is the start of the sentence.

3. Look at the last change before you look at the architecture
If it worked yesterday and it does not work today, the architecture is probably not the news. The news is what you touched.
git diff. git log -p --since='2 days ago' on the files in the stack. The config you “just rotated.” The dependency you bumped because Dependabot was orange. The feature flag you flipped in staging and forgot was also on in the shared environment.
I wasted a year of junior time believing every bug was deep. Most of my fast wins were shallow: a renamed field, a default that changed, an env var that was empty in the process that actually served traffic. Deep bugs exist. They do not get first look. First look is the diff.
If the last change is a dependency bump, bisect the bump. Do not “just pin it back” and walk away without knowing which transitive package moved. You will hit it again in a month. If the last change is yours, read it like it was written by someone you do not trust. That someone is you from Thursday.
4. Check the inputs, not the function you are proud of
Once I can reproduce and I have read the error, I stop staring at the algorithm. I print what actually arrived.
What was in the request body. What the ORM returned. What the queue job payload was after a retry. What time zone the timestamp was in. What the auth middleware put on req.user versus what I thought it put there. I have fixed more “logic bugs” by logging the argument list than by rewriting the function.
A pattern I use: one log line at the boundary. Incoming. Outgoing. Status. Request id. Not twenty logs inside the loop. The boundary tells you whether the bug is “we got garbage” or “we turned good data into garbage.” Those are different jobs. The first one is often not even your function.
Types lie too, in TypeScript land. A type says User. Runtime says {} because a serializer dropped the field. If you are debugging a typed codebase, believe the log, not the interface. I have watched people “fix the type” while production kept sending the empty object.
5. Cut the search space in half
If the inputs look fine and the last change is not obvious, I stop reading the whole file. I binary-search the system.
Disable half the middleware. Comment out half the job. Feature-flag off half the new path. In git, git bisect when the failure has a clear yes/no and a known good commit. Bisect feels like ceremony until you have used it once on a “it broke sometime last sprint” bug. Then it feels like having a second brain.
On the frontend, I ask: does it fail without the new CSS. Does it fail without the analytics script. Does it fail in a clean profile. On the backend: does it fail if I skip the cache. Does it fail against a local database with one row. Does it fail if I call the function with a fixture instead of the live client.
The goal is a smaller world. A smaller world is a bug you can hold. A large world is a vibe.

6. One hypothesis. One change.
This is the step I still skip when I am embarrassed. I want to be done. I change the null check, the retry, and the timeout together, deploy, and the error goes away. I do not know why. I have taught the system a superstition.
Write the hypothesis down. One sentence. “I think the job runs twice and the second run sees a missing row.” Then change the one thing that would be true if that sentence is true. If the failure does not move, the sentence is false. Cross it out. Do not keep the change “because it is cleaner anyway.” Cleaner is a different pull request.
I keep a short list of dead hypotheses in the ticket. It prevents me from reopening the same door after lunch. It also makes the eventual Slack thread useful: here is what it was not. Seniors look fast because they do not rerun the same failed idea. The list is how you get that without twenty years.
If you cannot name a hypothesis, you are still in step 4 or 5. More data. Smaller world. Not more edits.
7. Then the debugger. Then the model. Not before.
A debugger is a tool for a hypothesis you already have. I set a breakpoint where I think the story breaks. I do not step through a thousand lines hoping a variable will blush. If I cannot say which frame I want, I am not ready for the debugger yet. I am using it as a movie.
Same rule for AI. I will paste an error into a model after I can reproduce, after I have read the stack, after I have a sentence. I will not paste a 400-line file and “why is this broken” when I have not run it twice. The model is very good at sounding sure about a cause that matches the snippet and not the system. You become the person who ships that sure sentence.
When I do use a model, I give it the reproduction, the exact error, the one function, and the hypothesis I already killed. I ask it to attack the hypothesis, not to invent a new architecture. If it wants to rewrite the module, I ignore that paragraph. I am not here for a rewrite. I am here for the next measurement.
The shortcuts that still waste my time
Restarting first. Restarting is for “the process is wedged,” not for “I do not understand.” If a restart “fixes” it, you have a leak, a bad cache, or a race. Write that down. Do not call it resolved.
Logging inside a hot loop and then scrolling. You will hypnotize yourself. Boundary logs. Then a debugger on one iteration.
Fixing the test instead of the code because the test is louder. Sometimes the test is wrong. Assume it is right until you can prove the assertion is lying. I have “greened” tests that documented a bug and then shipped the bug with a checkmark.
Debugging on production data in my head. Pull a fixture. Anonymize a payload. Reproduce locally. If you cannot, say so, and instrument. Do not stare at a dashboard and invent a story that fits the chart’s shape.
Asking for help with “it doesn’t work.” Help starts after step 1 and step 2. Reproduction. Error text. What you already ruled out. That is not pride. That is how you get five minutes of someone senior instead of twenty minutes of them doing step 1 for you.
A 20-minute version of the same order
When the page is on fire, I still use the order. I compress it.
Two minutes: can I reproduce, even once. Two minutes: read the error and name the sentence. Three minutes: diff and last deploy. Five minutes: log the inputs at the boundary. Five minutes: one hypothesis, one change, in a branch I can revert. The leftover minutes are for deciding whether this is a rollback, not a hero edit.
Rollback is a debugging move. It restores a known world. Then you debug in staging with a reproduction. I have watched people “fix forward” for an hour on a bad deploy when a revert was a minute. Forward is for when you know the sentence. Revert is for when you do not.
What actually got faster
I spend less time in the file I like. I spend more time on reproduction and inputs. That feels slower for ten minutes and faster for the week. I also leave a trail: the sentence, the dead hypotheses, the request id. Future-me can pick up the trail. Past-me left a pile of wtf3 logs and a commit message that said “fix.”
The order is the same whether the bug is a null, a race, or a wrong env. The tools change. The sequence does not. Reproduce. Read. Diff. Inputs. Cut in half. One hypothesis. Then debugger or model.
If you take one thing from this: do not start in the function you are proud of. Start at the failure you can repeat. The proud function is where we hide. The repeated failure is where the bug is.