We've all been there, smoothly sailing/coding; getting closer and closer to the end result we have been working soo hard for. But suddenly the result is not what you expected it to be.
You maneuver your text-editor/IDE/etc. debugging, using test cases, and whatever move you can pull out of thin air to solve this issue. But to your surprise, you are stumped, unknowing of what to do next.
I currently find myself here in this limbo. What do you do to get out of this state?
Top comments (12)
Systematically touch the code until I see changes that give me more information about the bug. Often that means deleting code in chunks and re-running the program to see if I got the result I was looking for.
It can be a bit like a word search puzzle. Sure the fastest way would be to randomly scan the content, but the most consistent way is to go column-by-column until you find the trigger letters that can help you find the rest.
Oh man. One of my favorite topics :D Imma assume "google and sob over stack overflow" is already solidly in everyone's wheelhouses.
More mental-y debugging for trickier stuff that's really eating at your brain
Take a break. An actual one. Probably the most important step. If you're like me, you probably need to actually go to a friend's house, or at the very least, physically remove yourself from the ability to google and code more and perpetuating the perseveration that goes with this stuff. I'm really, really bad at this myself, and I think it's the hardest step.
Another big one: when did it last work? Are you sure? Did that last point deal with the part that isn't working now?
Talk to yourself. Out loud, and in comments. Remember, this doesn't have to be pristine--you can clean up the code later. Comment EVERYTHING. So many comments as you think "out loud" and walk yourself through. I don't mean this as a "God, you should have commented more" scenario--I mean as you're debugging, write down what you're testing. Why? What's weird? What's wrong? What's so bizarre about it? What have you tried? What haven't you? Write down EVERYTHING. Save a separate copy of the file to fill with this stuff--when I am HardCore tracking down a bug, I end up with mostly comments and code very sparsely interspersed. (Code-folding is a beautiful thing, because otherwise it's impossible to read then.) My debugging notes--I've got a DOOZY of one right now, but at least it's still at the exhilarating stage (side project, so that's a luxury)--often, at this point in the game, read like a study. I legit start to write down hypotheses, what I can test, etc.
Talk to people about it. This is common advice, but I think it's hard to know how to do it. For something really tricky, I find it helps to get a couple people--ideally of different levels.
-- Find one person with zero coding knowledge to listen, because they might make suggestions you would never dream of--and in explaining why those can't happen, you might realize that oh hey, they actually could, or in explaining why they really can't, you learn more about what's going on.
-- Find someone who has the same or similar amounts of knowledge as you. Similar frame of reference helps. I had a classmate from a semester behind me watch me code--I had some crazy-ass convoluted thing to switch casing and names for some columns of a table--and they went "Why wouldn't column aliases work?" and you know, they totally did, I had just totally forgotten they existed. Waaaay easier, too.
-- Find someone who knows more than you! Even if it's in a different language, or who just has more experience.
Corollary to talking to other coders--this one makes me crazy and makes the people I help crazy when I make them do it, because it's so annoying, but--read what the code says, out loud, line by line. Not what you THINK it is doing, or what it SHOULD be doing, just read EXACTLY what is in the code. Don't interrupt yourself to explain more--if you start having to genuinely justify it, that's prob not a great sign--and ask them not to interrupt you, unless one of you suddenly Realizes it. Human brains are SO GOOD at filling in the gaps and that is so fab and awesome and dammit, the computer is doing what you tell it to every single goddamn time, even if we don't reaaally want it to be doing that. I have been telling students in a class I'm helping out in all week when they feel dumb about making an error that I spent a good hour or so this week minimum trying to figure out why a query wasn't working... I had the line commented out. Yup, that'll do it! (Sigh.)
Print everything out. I know printing out error stuff is a "rookie" move--or I've certainly run into that attitude on a lot of sites (not this one!)--but man, sometimes the best thing to do is get rid of all the fancy debugging shit, and go line by line, and print out what the hell is in your variables, what's in them BEFORE, what's in them AFTER, where you are in the code. Go back to basics. If it's doable, and I'm assuming it is--again, this isn't something that's live (hopefully)--even get to the point of ye old printing out stuff like "About to enter the loop", "out of the loop", "just entered ABC function", etc.
Try and only change one thing at a time before you retest. I know how tempting it is especially if we start to think there's something that legit needs refactoring to start running with it--but resist. Change one. little. thing. and retest. The bug in my head right now I'm working on is in Scala, thus compilation, and part of a package, so I have to build it, then get it onto the VM that runs it, then delete the old version, rerun it, wait 5+ literal minutes while it runs and does what it does, and my computer totally doesn't have enough memory to be running the stuff on the VM that I need for it that I'm running so it takes ages. I reaaaaaaaally get the desire to do it faster but don't, because then you can't actually start to untangle it.
If you're starting to grasp at straws--break it on purpose. Get it to start throwing errors. Especially when things AREN'T happening that should be, or when nothing is showing up and everything just dies silently, or is Off, start making errors on purpose. CLEARLY mark them. Especially for the really creepy quiet errors that just peter out and gasp and you don't quite realize it until everything is just... not... it can be really disorienting, and I legit find it really reassuring to start doing ridiculous stuff to reassure my coding lizard brain that The Laws Of Coding Nature are not broken and that yes, Java WILL yell at me for not declaring some type, and that this isn't the coding equivalent of when you wake up in the middle of a weird nap and it's too quiet and you wonder if the world has ended and this is maybe a freezeframe--it sounds dumb, but it can be really reassuring to just get in there and make sure that things that SHOULD fail do fail, that things that should error are erroring, that strings of a known length are the right length, all that stuff.
Write down literally every idea you have, no matter how unrelated or stupid it seems. It probably isn't, and even if it is... well, it's not, because you get data regardless, right? Like -- I'm working on a weird one, in a language I don't really know on a system I don't really know--so needless to say, the error is on my end, and it's probably Mistake vs a Slip (friend of mine taught me that one) too--and I got a big breakthrough in it when it a fit of annoyance I basically went "OKAY WELL, FINE, THEN SHOW ME THE DAMN DATABASE EVERY TIME YOU GO INTO THAT LOOP AND WE'LL JUST START --oh. The... the database... doesn't... exist... anymore in the... loop. Okay. Well, that's Disturbing to say the least, but that *does* mean that it makes sense the query isn't working..." If I had been trying too hard, I never would've done something as ridiculous as checking to confirm the database I had been querying succesfully in a variety of ways with the same line of code lines earlier still existed--I mean... why would I? And clearly, it was the right choice. (Hey, I wasn't kidding when I said this one thing I'm trying to figure out is getting really weird...)
If you're still really stuck and you can't break out of it, save a copy of the file--sure, start another branch, whatever, but also just save a damn manual zip or whatever and stash it somewhere, hell, seriously email it to a friend depending on how worked up you are--and start over, either that function/method/section or what. It's very Burn It To The Ground, but sometimes that's what you need to do--very rarely is it the right thing code-wise, but usually as you start redoing it, you'll go "OH. WAIT--" and then you'll be super glad you saved a copy. (Depending on how upset and emotional yuo get, seriously email a zip to a friend. Sometimes when we get REALLY WORKED UP about a thing, I know I at least will rage delete my own code--thankfully I've gotten much, much better about versioning--but the only thing more heartbreaking and painful is having legit erased stuff out of fury and frustration and then thirty minutes later realized it and try to recreate it all from scratch.)
-- Change your syntax highlighting. Try a bunch of different ones in vaguely rapid fire. They all do different things, even if you hate them, the visual novelty can help you see new stuff.
-- Reformat your code. Beautify it, add stupid linebreaks (where it won't break), make the font huge, resize the window, whatever. Again, visual novelty! There's a really good reason the person who sees that missing semicolon instantly isn't the person who's been staring at it for a long time.
-- Linter, if applicable. Even if the issue is beyond a syntax one, even if you hate linters, seeing stuff brought up in a different manner can help.
-- Are you in the same running environment? if not, what's different? Same build environment?
-- Are you working in the right files/branch/whatever?
-- Do you have assignment lurking somewhere in a conditional?
-- Operator precedence! We're used to this one for math stuff but not for others I think.
-- Any chance you're looking at the wrong documentation--whether too new or too old?
(1) Totally did this once because I need a Break badly.
Ha! I've done all of these. Nice list. You should think about writing a full article to this effect. :3
Here's a few more things I do.
(Incidentally, I also sometimes print source when I'm reading particularly complex source written by other people.)
Desk check! Get out a piece of paper and become the compiler. Start reading through the source code exactly as the computer would. Resist the urge to skip forward. Take nothing for granted. Write down each variable name, type, and value, and change it exactly as instructed by the code. 9 times out of 10, this allows me to catch the error.
Break out the LEGOs. Sometimes a paper-based desk check isn't enough. For particularly complex algorithms, I'll use my LEGOs and other desk toys to fill in for certain code structures, elements, pointers, objects, etc. There's no rules to this: just do something that makes sense. This is especially helpful when debugging search and sort algorithms. Besides that, it's fun.
I have to emphasize one of the points already made: talk to a non-coder. My mother, who is also my business partner, knows just enough about programming to help oversee project operations, but she isn't a coder by any means. On one occasion, after three days of my trying to debug something, she walked in and said "I bet you forgot a semicolon".
Yup. I'd forgotten a semicolon - right in the middle of a for-loop definition - thereby creating an infinite loop.
Thanks! :D I really like the idea of printing it out. I always forget about paper...
What a fantastic read, it would be worth a read as a stand alone article.
Clean it up
In addition, what I do to solve hard-to-find bugs, is to clean up the code. More often than not, the code is a mess with duplicate code, unneeded variable, wrong naming etc. Cleaning up gives me a chance to look differently at the source and sometimes - when your lucky - cleaning up actually solves the problem.
Tear it apart
Other option is to take out portions of the code and test them separately. When doing this, I often place the code back in its own function since it is more or less abstracted away from that point on.
a few tricks:
One trick is to read through your code as if you were explaining it. Try to explain what each line of code is supposed to do, and what each section of code does. when the code stops making sense, look into that bit of code more closely... think of it as sort-of teaching yourself to code/ how your program works, in the third person. The important thing is to read the code.
Another trick that sometimes works for me is to look at variables at various stages of a process. for example if you are getting a strange result, try looking at each stage of that process that leads to that result, and try to find something that seems out of place.
Finally a great way to fix a stubborn bug is to have others look over your code. This is especially true if you are new to programming, but it never hurts to ask for a second opinion.
If it's been killing me, I'll grab another dev if they're free for a round of ping pong and talk through it. Rubber duck debugging has never failed.
I normally start with console logs, to see what data looks like around the function and try to narrow down the problem error - that's the most important step for me. This normally works.
Once I've got it narrowed down, and it's not fixed, I pull the debugger out and step through each line of code. Chrome is great for this, you can just put the line
debugger;in your code and it'll pop up in chrome.
If that's not fixed it, I'll either leave it for the day (I've often come up with solutions on my commute home) or I'll blame a race condition
Funny thing. I've been in this situation, for instance, when inheriting a big, messy code base, something I don't do often anymore.
When this happens I go to the good old printf/console/whatever you have to work your way building your understanding of how the code goes, and you better be patient!
If your code is very big and/or messy AND multithreaded… well, you're in a bad spot! The best way to fight bugs in this kind of situation is by REWRITING. Isolate, create interfaces, bridges… It's basic code surgery and there's a terrific book about this "Refactoring", by M. Fowler.
If this is your own code, then you've still got to learn more about designing good code. When you really take care of learning… at first it might be daunting, I know… but hold on, it will come through the years and you will cease to fight.
You will always make mistakes but most of them will solve easily :)
If the bug can be tackled by divide-and-conquer, then I try that tact. Isolate the bad code into a small toy app. Or through a targeted integration test.
(Unit tests are typically too fine-grained to trip the bug, until you know what the bug is. Then, perhaps, you could write a unit test after-the-fact that would catch the bug... if it was a unit scope of bug rather than an integration or systems scope of a bug.)
Otherwise, I use "first grade debugging" techniques of print statements or trace points, and try to discover where the code is going awry.
If the bug appears sometime along the way as the code changes and evolves, and I have change history, I sometimes will use a binary search to see what particular check-in caused the bug. (Sometimes with the relief that it wasn't me... sometimes with dismay that it was me.)
Some great advice here, but I think it's worth going elementary here and remind people to always first confirm and replicate the bug. Once you've done that it's usually trivial to write a test case. With a test case you can then easily step through and debug at a sufficient level to resolve the issue.
On another, yet similar, note, when resolving the bug don't even try to do it without having sufficient test cases touching the code you're changing. A colleague, and one of the best programmers I've known, coined the term "refucktoring" for when you're refactoring without sufficient test coverage.
I'm only part-time committed to TDD, but pretty much 100% committed to it when it comes to bug fixing. It works!
On a side-note, I'm one of those perverse developers who actually quite enjoy digging into and fixing bugs.
First, check the source control annotated history to see if I can pass it on to somebody else. :P
My approach to hard-to-fix defects generally isn't different from easy-to-fix defects; the extent of the effort differs however. The various techniques I use are:
debug statements I start emitting values everywhere near the area that is causing problems. Go up the stack, add more debug statements. I like having temporal traces of the system when odd things happen.
actual debugger I generally only use a debugger to get a back trace at the point of an exception or OS fault. I find their value otherwise to be quite limited; either the problem is simple enough I don't need one, or it's too complex for stepping to actually help.
unit tests I write new unit tests for suspicious code: code that seems like it might not work as intended, or has some unusual side-effects. If a new unit test isolates the defect, then great, if not, then it improves coverage.
code breaking I introduce intentional defects in, or remove features from, the code to test conditions elsewhere. This helps find checks that aren't being triggered correctly, or identifies places where bad values slip through.
refactor Bit by bit I'll refactor messy code in order to isolate the defect. Sometimes this fixes the defect on it own.
In all cases I tend to have a pad of paper scribbling short notes (depending on complexity). I repeat the above techniques until I've figured out what the bug is, then continue until it's fixed.
I try to reorganize my commits. Ideally new passing test cases get added first. Then all refactoring that doesn't change anything -- but if new tests pass add them to those commits. Then actual fixes come last. This helps in code review, and also creates code improvements even if the defect isn't found, or there's a disagreement on its fix.
I usually try not to jump into coding right away. I open a new note on Evernote and I write down everything I know and/or figure out about that particular bug. I reproduce it and write down the steps I did to reproduce the bug. Then I start to brainstorm what might be the cause. This envolves looking at the code and/or debugging. Then I think about a possible solution, and work my way up from there.