When I joined my team at Comcast last summer, my manager gave me some great advice about how to be valuable as a developer new to a project. I knew I probably wouldn’t be writing any major features at first, especially since the team I joined was knee-deep in trying to meet a release deadline and I knew very little about the application or the company’s APIs. One way to be valuable, he said, was to become good at debugging. It sounded like a good idea at the time, and as I’ve tried to develop my debugging skills, I think it’s some of the best advice I’ve received about how to be a good developer.

If you read Hacker News, watch a lot of conference talks, or listen to developer podcasts, you’ll notice that most of the time, the exciting things we talk about are new. And while learning new things is fun, getting good at improving your existing processes is also important. So while I’m sure I’m missing some techniques in my debugging toolkit, here is the checklist I typically go through when I’m debugging.

Debugging while working on a new feature

First, I’ll take a look at the error messages I’m getting (if there are any). Sometimes it’s clear why my code isn’t working. Perhaps I’ve mistyped a method name or used a curly brace where I meant to put a square bracket. I found an extraneous line break in a long string today that led to a FunctionClauseError in my Elixir code. Some error messages are written better than others though, and sometimes the error message points at a symptom of the problem, not the root cause itself.

If reading the error message doesn’t solve my issue, I’ll look up the error message online and see what I can find on Stack Overflow or GitHub. Chances are, I’m not the first person to wonder about an unexpected error.

Next, I’ll throw in a breakpoint so that I can stop its execution and see that I’m getting the values I expect to get at various stages in my code. binding.pry is invaluable in Ruby, and using debugger and the developer tools in your browser are essential JavaScript skills. While I haven’t found these kinds of tools to be quite as mature in Elixir, IO.puts has been very useful in my first few weeks with the language.

Most of the time, I’ve resolved the error by this point. But debugging probably follows the Pareto principle: 80 percent of debugging time is caused by 20 percent of the bugs. So what to do next after there doesn’t seem to be an answer in a forum online?

A good next place to turn is the documentation. A closer look at a function definition and the available options often reveals a way to solve the problem. If anything, reading the documentation and looking at source code is a good gut check to make sure you understand how to use the code you’re invoking.

At this point, I’ll take another close look at the code to make sure I’ve exhausted all of the possible approaches to the problem that are obvious to me. At this point, I’ll usually ping someone on my team on Slack and see if they have a few minutes to look at the issue. As best as I can, I’ll try to explain the problem I’m having, the results I’m expecting, the results I’m getting, and the approaches I’ve tried so far. I’m lucky to work with some patient, talented, and experienced developers, so they’ll often have a solution or at least another possible plan of attack that I haven’t thought of. Sometimes I figure out the problem while my pair serves as a good rubber duck.

Finally, if I can’t figure it out with someone else, I’ll try to take a break and get some coffee or move onto another task. Allowing my brain to work on something passively in the background will often help me get closer to solving the problem.

This approach has worked well for me so far and I find myself getting stuck a lot less than I did when I first started out. As I’ve gotten more experienced, I think I’ve also gotten more comfortable with saying to myself, “You can solve this in as ugly a way as you want right now. Unit tests are your friend and you can always refactor later.” Feeling comfortable with duplication and overly simple methods sometimes helps get me to working code. Once I get the behavior I expect, refactoring is a lot less stressful. There’s no need to solve a bug perfectly the first time you try.

Debugging legacy code, or more generally, anyone else’s code

On a bigger project, a lot of the time I spend debugging will be spent looking over someone else’s code. I might be collaborating with them on a feature or just being their rubber duck. I’ll try the steps above, and if my teammate is available, I’ll ask him or her to talk me through their code. Talking about the problem often helps me understand why they chose a particular approach. Why they wrote code that produced a bug, for example, becomes a lot more clear when you realize the constraints they had or the specifications they were given.

If I can’t get a hold of the person who wrote the code, git commands and code review discussions are really helpful. git log, git show, and git diff between two commits help highlight the evolution of the code in question. They’ll often show why code changed in a way that may have produced the current bug but helped resolve a previous one. Knowing your git history is a good way of not repeating mistakes.

Debugging often feels to me like a haphazard random process. Sometimes the answer to an error is buried in the middle of a years-old GitHub issue thread. But following the workflow above has helped me standardize my approach to solving these knotty problems and has made me a more efficient developer. I’ll continue to refine this approach, but I’d love to hear if there are any techniques I haven’t mentioned that you find valuable.

Code not working is the norm in web development, whether that’s because the unit test you just wrote is failing or because you found a bug in production. Getting better at debugging is one of the best investments you can make.