An Introduction to the Debugger

Johan Boberg
Engineering at Earnest
8 min readSep 6, 2017

--

The debugger is one of the most powerful, but underutilized, tools in a developer’s toolbox. The name “debugger” suggests that this tool can only be applied when dealing with bugs, but in reality it can be used for so much more.

The information one gets from debugging is useful even when there is no problem with the code. It gives unique insight into how a program runs and allows one to gain a much deeper understanding of the code that is being developed. It also allows one to trace running code, and inspect the state and flow of the execution.

Some debuggers even support hot swapping or live edit, which makes it possible to alter the code while it’s executing, saving you from having to restart your application for each change you make.

Coming from a Java background, which has excellent debugging support, and a university that focused on practical skills like using a debugger, I never use println or console.log when trying to find and fix a problem. Instead I fire up the debugger, which is both a faster and an easier way to find the code that is not working compared to printing debug data to the console and restarting the app for each insight.

I’ve used a debugger in Java, Scala, PHP and JavaScript and they all work, more or less, the same. With this post I want to show you how to get started with the debugger and what you can do with it. The focus is on applications and backend services but most things also apply to frontend code.

When to use the debugger

Ok, I lied. I, too, use println and console.log when debugging. My rule is this: If I can't figure out the problem within two runs of the program I switch to the debugger. The secret is to make it as easy as possible to use the debugger so that the threshold to switch is as low as possible. This involves:

  • using the debugger often so that it’s a familiar environment.
  • making sure debugging is setup for the project so it’s just a matter of clicking “Debug” in the IDE (or similar).
  • having a debug strategy, as it takes some time getting used to debugging using a debugger.

Debugger setup

Here are some recommendations for the JVM and Node, which are the two technologies I’m most familiar with.

JVM

For the JVM I would recommend using Eclipse or IntelliJ IDEA, since debugging works right out of the box with these IDEs.

Node

For Node I would recommend WebStorm, since debugging works without any additional setup here as well. Although I haven’t used Eclipse for Node I believe that should work too, possibly with a bit more work. I have also heard positive comments about Visual Studio Code.

If you prefer to use any other editor there’s an excellent post by Paul Irish that shows you how to get up and running with Chrome DevTools, which is not tied to an IDE. Note that the flags mentioned in that post differ slightly depending on the version of Node you’re using.

Unfortunately the experience using Chrome DevTools is not as good as the other alternatives (at least for backend services). I would therefore encourage you to try the IDE approach.

Debugging strategies

There are a couple of alternatives for how to initiate a debugging session. Depending on what your application looks like, one can be easier than the other.

Debug unit tests

If you have a unit test that covers the code that’s not working you can use that as a springboard to launch a debug session. This is the easiest way to get debugging working as it doesn’t require you to spin up the whole service with all its dependencies and allows you to focus on the code that is not working. If there’s no unit test that covers the code you want to debug, you can always create one. It can even be temporary.

If you’re using an IDE, to start the debugging session right-click on the file containing the unit test (or even a specific test if your IDE supports that) and select Debug. Your debug session should start and finish right away since we haven’t set any breakpoints yet.

If you’re using Chrome DevTools and Mocha as your test framework you can use the following to launch a debug session:

node --inspect --debug-brk ./node_modules/.bin/_mocha --timeout 0 test.spec.js

Add the source code folder to your Chrome DevTools workspace and add breakpoints as necessary.

Debug main entrypoint

Using the main entrypoint of an application to start the debug session follows the same approach as using a unit test. However, it has some potential drawbacks. For example, this approach might be more cumbersome because you have to interact with the application to trigger the error condition. Your application’s run-time dependencies could also prevent you from using this approach.

As with the unit test strategy, right-click on the file you want to debug (in this case the main entry point to the application) and choose Debug to start the debugging session.

The equivalent for Chrome DevTools would be:

node --inspect --debug-brk app.js

Remote debugging

A third alternative is remote debugging. This might be required when debugging running systems or when writing a unit test is not feasible.

Refer to the manual for your IDE for how to set this up. Essentially, this requires you to start your application in debug mode so that it will listen to a debug port to which you can connect.

This is actually how Chrome DevTools works so there’s no need to do anything special here.

Features of the debugger

I’ll be illustrating the features with screenshots from JetBrains’ IntelliJ and WebStorm IDEs but other debuggers should work similarly.

Breakpoints

Breakpoints, as the name suggests, make the debugger stop the execution at the location of the breakpoint. Once stopped, you can explore the current state of the code, modify the values of variables, execute arbitrary code (see Watches and Expressions) or step to the next statement. Once you’re done exploring, you can hit “Play” to continue to the next breakpoint or to the end of the program if there are no more breakpoints.

Here is a screenshot of when the debugger has stopped at a breakpoint. The entrypoint is a unit test (it, not shown), which calls addOne, which in turn calls sum. Values of variables in scope are displayed inline but are also listed in the Variables window. You can also view the value of a variable by hovering over it.

You can assign conditions to breakpoints to specify when they should trigger. E.g., if a problem only happens on the 50th iteration of a loop, you can set the breakpoint to trigger only when a variable contains a certain value or simply when the breakpoint has been hit 50 times. What features are supported depends on the debugger you’re using.

Breakpoint settings

In IntelliJ and WebStorm you can get to the settings by right-clicking the breakpoint. Click More for all settings (shown above).

Call stack

Once you’ve hit a breakpoint you can inspect the current call stack and see how the current method was called and what the state of the code looked like all the way from the entry point of your program.

Here’s what it looks like when going one level up in the call stack from the previous breakpoint. Notice that addOne is highlighted in the Frames window, and the current variables in scope are shown in the Variables window.

Stepping

Once you’ve hit a breakpoint, you can start stepping through the code, one statement at a time. There are a couple of ways you can do this.

Stepping buttons in WebStorm

Pro tip: Take the time to learn the keyboard shortcuts for the various stepping strategies. It will save you a lot of time in the long run and make it easier to use the debugger (plus you’ll look cooler 😎).

Step into
If the statement you’re currently on is a call to a method, selecting “Step in” will make the debugger step into the method and break on the first statement inside the function.

Step over
Conversely, “Step over” will make the debugger skip stepping into the method. The code inside the method will still be executed but the debugger will not break until the method returns.

Step out
If you’re inside a method, “Step out” will make the debugger continue execution until the method has returned and break on the statement following the call to the method.

Run to cursor
If you want to skip a number of lines and don’t want to having to press “Step over” a number of times, right click on a line and select “Run to cursor” to make the debugger break at that line.

Expressions

Expressions are one-off pieces of code that you can run in the current state of the code. For example, call a method with a variable in scope.

Watches

Watches are just like expressions but evaluated every time you step to a new statement. In IntelliJ and WebStorm, watches are shown in the Variables window. Eclipse has a separate window called Expressions.

You can add watches by highlighting a statement, right-clicking on it, and selecting Add to Watches (or similar). Alternatively, you can define arbitrary watches directly in the Variables window.

Gotchas

The debugger affects timing and hence might affect concurrent programs. This can also be used to your advantage by strategically placing breakpoints to force a certain execution path.

The debugger also slows down the execution, especially in JavaScript, which you might notice if you’re debugging a long loop or similar.

Troubleshooting

The debugger hangs waiting to connect

Are you connecting to the right port? If you’re using Docker containers, is the debugging port exposed? Check with docker ps.

Breakpoints are not picked up

The sources might be out of sync. This might happen if the editor is pointing to one set of sources but you’re executing another set. In IntelliJ and WebStorm, look for the checkmark inside the breakpoint icon, which indicates that the breakpoint is recognized by the debugger.

Happy debugging!

You should now know the basics of debugging and be ready to debug your first program. Good luck!

See also

A great guide to debugging is JetBrain’s help section on debugging. It goes through everything mentioned in this post in more detail and also contains more advanced topics such as hot-swapping/live edit, and how to debug multi-threaded and long-running processes.

--

--

Johan Boberg
Engineering at Earnest

Not a sinner nor a saint when it comes to software development. Devoted covert anomaly hunter.