10 Ways to Debug your JavaScript

— 10 minute read


I saw a tweet recently (unfortunately I can't remember which one! I found it! I've embedded it below 👇!) that mentioned someone demonstrating to a colleague how to use the debugger statement in dev tools and how it blew their mind. I thought it would be useful to run over all of the other hidden ways we can debug our JavaScript code in Dev Tools.

By debug I really mean break into our code and see what's happening at a specific point.

The Humble alert Statement

Back in the day, before dev tools were even a thing, we had to resort to littering our code with alert statements to see what was happening. It gives me shudders just thinking about it - I hope none of you are still doing this. If so, please read on and find out the other options! Having said that, it occasionally has it's uses :)

const x = 0;
const y = 0;

for (let z = 0; z < 10; z +=1) {
alert(z);
}

console.log FTW!

console.log and all of its friends (.warn, .error, .table, .group, .time) are so much more than the next iteration of alert. They are really good for giving you a snapshot of a selection of variables, but aren't so good for helping us understand program flow because they are a snapshot of a point in time which has now changed completely.

const x = 0;
const y = 0;

for (let z = 0; z < 10; z +=1) {
console.log(z);
}

I will often use console.log to output some info and then use it as a shortcut (click the file/line number link to the right of the output) to go to that particular place in the code and add manual breakpoints.

Screenshot of Chrome Dev Tools showing a console.log statement with a link to 'Test Snippet, Line 5' on the right hand side

Set a Breakpoint Manually

For me, setting a breakpoint manually and stepping through the code is the best way to figure out issues and understand the full program flow. Doing so lets us see the state of the program at that specific point in time and make debugging decisions before it changes, which console.log doesn't let us do.

Added breakpoints are usually persisted between page loads so you can make changes, refresh and get back to where you were quickly, which makes them very powerful.

The only downside I find is when dealing with transpiled output and source maps it's sometimes impossible to select the right line due to something which can be frustrating. If that happens to you then I tend to use the next option...

Use the debugger Statement

The debugger statement lets us drop a breakpoint in our source-code which the dev tools will then pause on when it hits that line. This solves the problem I mentioned above because it is baked into the output so there's no way around it for the browser!

const x = 0;
const y = 0;

for (let z = 0; z < 10; z +=1) {
debugger;
console.log(z);
}

This technique is also useful for when you can't be bothered hunting down the same file/line in dev tools to add a manual breakpoint or if there isn't time to add a manual breakpoint before your code triggers (for example, if you've a script running immediately after page load).

Conditional Breakpoints

Sometimes you're only interested in debugging a function in specific circumstances. For this we can add conditional breakpoints which will only pause execution if a certain condition evaluates to true. To do this we right click on the line we want to break on, choose 'Add Conditional Breakpoint'.

Screenshot of the menu when right clicking on a line number in Chrome Dev Tools' Source tab. It shows the 'Add conditional breakpoint' option highlighted as the second option

From there a wee text box will appear where we can enter a condition:

Screenshot of the text box shown after clicking Add conditional breakpoint, it contains the code 'z % 2 === 0'

We can also use the debugger statement in a similar way by wrapping it in a regular if statement. For example

const x = 0;
const y = 0;

for (let z = 0; z < 10; z +=1) {
if (z % 2 === 0) {
debugger;
}
console.log(z);
}

Event Listener Breakpoints

Slightly more niche is the Event Listener Breakpoints which allows us to stop the code when any of the selected events are triggered. This is really useful when you're not sure what code is triggering some behaviour. For example, you want to know what is causing the page to scroll at a random time.

By ticking the Control > Scroll event checkbox, the code will pause and you will be taken to the place where the scroll was triggered.

Screenshot of the Event Listener Breakpoint pane of Chrome Dev Tools showing a list of event categories. The 'Control' category is expanded and the 'Scroll' item checked

You can try this by dropping the following code into the console:

document.body.scrollTo(0, 1000);

DOM Breakpoints

Another type of breakpoints is ones that can be attached to DOM nodes themselves. Like the Event Listener breakpoints we talked about above, this is good for tracking down code which causes changes.

Screenshot of the right-click menu of a DOM node in the Chrome Dev Tools' Elements tab. It shows the 'Break on' option expanded showing the 'subtree modifications', 'attribute modifications' and 'node removal'

Subtree Modifications

If you right-click a node in the Elements tab expand the Break On menu and click subtree modifications then whenever a child of that node is altered in any way (node removed, attributes changed etc) then the code will be paused and you'll be taken to the line which triggered it.

Attribute Modifications

The attribute modifications option lets us restrict it to just modifications of a node's attributes. This is useful for figuring out when classes are added or removed.

Node Removal

Finally, we can choose node removal which will pause our code when that particular node is removed from the DOM. Again, this is useful for figuring out what code is messing things up!

Pause on Exceptions

Another often overlooked option is the Pause on Exceptions toggle. When enabled this will pause our code when it encounters an unhandled exception allowing us to see straight away the conditions that caused it to happen and poke around at the variables' values. Disabling this option will just log unhandled exceptions to the console and continue on its way.

Screenshot of the Chrome Dev Tools' debugging toolbar showing the Pause on Exception option highlight. It also shows a checkbox below it, labelled 'Pause on caught exceptions'

Try this by adding an exception to our code snippet:

const x = 0;
const y = 0;

for (let z = 0; z < 10; z +=1) {
console.log(z);
throw 'error';
}

To take things further by enabling this option a checkbox will appear allowing you to Pause on caught exceptions as well. This means that the code will pause even when an exception is handled by a try/catch block. This is useful if exceptions are being swallowed somewhere. The only downside is it can result in tons of pauses in external library code as it runs.

If we wrap our throw statement in a try/catch block and rerun it then it won't pause anymore cos the exception has been handled. However if we check the checkbox and run it again we will see it pause once more.

const x = 0;
const y = 0;

for (let z = 0; z < 10; z +=1) {
console.log(z);
try {
throw 'error';
} catch(e){}
}

Managing Breakpoints

Now that you've got breakpoints littered throughout your codebase you might want to disable some or all of them quickly. The nuclear option is the Disable Breakpoints toggle which turns all breakpoints off or on at once.

Screenshot of the Chrome Dev Tools' debugging toolbar with the 'Deactivate Breakpoints' button highlighted

Alternatively you can toggle breakpoints off and on individually using the checkboxes in the Breakpoints list. This means you can disable them without actually removing them and having to find that location again to re-add them.

Screenshot of the Chrome Dev Tools' Breakpoints panel listing two breakpoints from our sample code. The first has a checked checkbox meaning is is enabled and second has an unchecked checkbox meaning it is currently disabled.

Filed under