The main task of a web developer, I would say, is solving problems. In any project, we are always in a constant cycle of building things, finding issues(sometimes without looking) and coming up with the best possible solution. To achieve this, modern browsers have provided us with a vast array of free tools that are available for us to learn and use. In this post, I will give you an insight to two of the tools that I use on a daily basis: the debugger and the console. I chose to cover these two first because, from my point of view, those are the most basic ones that every developer should know and use.

To demonstrate the correct usage of these tools, I will be using a React sample project called: React Calculator. You can click that link and it will take you to the Github project, in case you want to download it and follow along with the tips.

Console

The console is the most used JS debugging tool. It basically allows you to print whatever you want to the browser's console once the code reaches that line. Let's take this example: I'll drop a bug into the React calculator's code to crash the app after I try to add up two values.

Figure 1. Console Error

To find what's going on, I'll place console.log's into my code. In the App.js file, I'll change to the following:

handleClick = buttonName => {
    const calculatedValue = calculate(this.state, buttonName)

    console.log(buttonName)
    console.log(calculatedValue)

    this.setState(calculatedValue);
  };

  render() {
    console.log(this.state.total)

    return (
      <div className="component-app">
        <Display value={this.state.next || this.state.total || "0"} />
        <ButtonPanel clickHandler={this.handleClick} />
      </div>
    );
  }

After we try to adding 1 to 1, we get this printed to the console

Figure 2. Debugging through the console
handleClick = buttonName => {
    const calculatedValue = calculate(this.state, buttonName)

    console.group()
    console.log(`Button name: ${buttonName}, type: ${typeof buttonName}`)
    console.log(`Calculated value: ${calculatedValue}, type: ${typeof calculatedValue}`)
    
    this.setState(calculatedValue);
  };

  render() {
    console.log(`Total: ${this.state.total}, type: ${typeof this.state.total}`)
    console.groupEnd()

    return (
      <div className="component-app">
        <Display value={this.state.next || this.state.total || "0"} />
        <ButtonPanel clickHandler={this.handleClick} />
      </div>
    );
  }

If you are not familiar with console.group() and console.groupEnd() , they are very helpful to group your logs into blocks, making it easier to see what is happening inside the app. This is the output of the above code after adding 1 to 1:

Figure 3. Using console.group() and console.groupEnd()

The logs are now grouped into blocks that are easier to read. We can infer from the console that the user first typed 1, then +, then 1 and finally =. We can also see, that the calculatedValue is an object, and when the user clicked "=", calculatedValue was assigned to total and then the app crashed. This is because the Display component is rendering the total value (which is an object) directly and, as the error in Figure 1 stated:

Objects are not valid as a React child.

Now that we know that the calculatedValue is indeed an object after we click on the = sign, let's debug the calculate function. For this, we will use the debugger.

Debugger

To use this tool, we just need to type the debugger keyword wherever we want the code to stop executing. When I use it, I like to drop a few of them across the code to make sure that my function stops where I suspect the problem is. For example, I'll drop one right when we enter the calculate function, one when the if statement detects the usage of the = sign (because the code breaks after we click on this) and a last one when entering the operate function (called inside calculate).

// src/logic/calculate.js
export default function calculate(obj, buttonName) {
  debugger // DEBUGGER HERE
	.
	.
	.
	if (buttonName === "=") {
    debugger // DEBUGGER HERE

    if (obj.next && obj.operation) {
      return {
        total: operate(obj.total, obj.next, obj.operation),
        next: null,
        operation: null,
      };
    } else {
      // '=' with no operation, nothing to do
      return {};
    }  
	.
	.
	.
  }

// src/logic/operate.js
export default function operate(numberOne, numberTwo, operation) {
  debugger // AND DEBUGGER HERE

  const one = Big(numberOne || "0");
  const two = Big(numberTwo || (operation === "÷" || operation === 'x' ? "1": "0"));
	.
	.
	.
}

When we run our program, and click on the = calculator's sign, this will happen

Figure 4. Inside the debugger

The code's execution is paused on the specific point where we drop the debugger and we can access the variables there to have a much better idea of what is happening in the code. As you can see in the first image, we have access to the local variables under the Scope tab. You can even change these values if you want to experiment with them. You can also hover over the variables to see the values assigned to them. Since we haven't caught anything odd, let's click on the Play icon to resume the execution of the code.

Figure 5. Watch's tab for the debugger

In the last screenshot, you can see that the execution paused at the next debugger keyword. This time, we are switching tabs from Scope to Watch. In this tab, you can add variables that you are interested in monitoring. I added obj.next , obj.total  and obj.operation . Everything still looks OK at this point, so let's click on the play button again to get to our last stop.

Figure 6. Debugger navigation controls

Now we are inside the operate function. Let's cover the basic navigation buttons on the debugger feature. The first arrow "steps over the next function call", the down facing arrow is to "step into the next function call", and the up facing arrow is to "step out of the current function". First, we are going to step into the next function call to see what happens:

Figure 7. Exploring Debugger's navigation

After clicking on the down facing arrow, it took us inside the Big(numberOne) function. For this example, we are not interested on what happens inside this function. To step out of it, click on the up facing arrow.

As you can see in the right image, we are back in the operate function. In this case, the "step over the next function call" arrow (the first one) will be the most helpful of the three, since it will move line by line in operate without going inside any function evaluation.

Figure 8. Watching specific variables

After clicking several times on the "Step over the next function call" arrow, I reached the line where operationResult is being calculated. Note how I registered three more variables in the Watch tab, which are the ones that I want to monitor. From this, we can see that the value of operationResult is what's causing the problem. It is an object of type Big, and it should be a string. This means that we are missing a toString() method. This is breaking the app when trying to add two numbers.

Figure 9. Validating our solution using the debugger

I applied the change and added the debugger right after operationResult is calculated just to validate that this was indeed the problem. As you can see, operationResult is now a string .

I'm a strong believer that every JS developer out there should know how to use these tools. They make the debugging process much easier, and as I mentioned at the beginning of this post, we are going to face all kinds of challenges on a daily basis; so it's worth spending some time mastering them. I hope that this post will help you on your web development journey when encountering the most daunting bugs hiding in your projects and motivates you to keep sharpening your debugging skills. In future posts, we will be delving into more complicated parts of the devtools like the Network and Performance tabs. Happy coding!