Mastering the Art of Debugging: Techniques for Efficient Troubleshooting ๐ŸŽฏ

CoderDev
Dev Genius
Published in
12 min readMay 1, 2024

--

Debugging article thumbnail

As developers, weโ€™ve all been there โ€” our code isnโ€™t working as expected, and weโ€™re left scratching our heads, trying to figure out what went wrong. Debugging is an essential skill that every programmer must master, as itโ€™s often the key to solving complex issues and delivering robust software solutions. In this article, weโ€™ll explore various debugging and troubleshooting techniques that can help you become a more efficient and effective problem-solver. Buckle up and get ready to laugh (and maybe cry a little) as we navigate the wild world of bugs and glitches!

๐—ง๐—ต๐—ฒ ๐—œ๐—บ๐—ฝ๐—ผ๐—ฟ๐˜๐—ฎ๐—ป๐—ฐ๐—ฒ ๐—ผ๐—ณ ๐——๐—ฒ๐—ฏ๐˜‚๐—ด๐—ด๐—ถ๐—ป๐—ด

Before we dive into the techniques, letโ€™s appreciate the significance of debugging. Writing code is an art, but itโ€™s not uncommon for even the most experienced developers to introduce bugs unintentionally. These bugs can range from simple syntax errors to complex logical flaws, and they can have far-reaching consequences if left unresolved.

Effective debugging not only helps you identify and fix issues but also enhances your understanding of the code and the underlying concepts. Itโ€™s a learning experience that can help you write better code in the future and avoid repeating the same mistakes.

Fun Fact: According to a study by the University of Cambridge, developers spend approximately 50% of their time debugging code!

So if youโ€™re not spending half your life pulling your hair out trying to fix bugs, are you even a real developer?

Before diving into the world of debugging letโ€™s first have a glimpse of what we are getting into.

Table of Contents

  1. Technique 1: Print Statements and Logging
  2. Technique 2: Debuggers
  3. Technique 3: Unit Testing
  4. Technique 4: Code Review and Pair Programming
  5. Wrapping Up

๐—ง๐—ฒ๐—ฐ๐—ต๐—ป๐—ถ๐—พ๐˜‚๐—ฒ ๐Ÿญ: ๐—ฃ๐—ฟ๐—ถ๐—ป๐˜ ๐—ฆ๐˜๐—ฎ๐˜๐—ฒ๐—บ๐—ฒ๐—ป๐˜๐˜€ ๐—ฎ๐—ป๐—ฑ ๐—Ÿ๐—ผ๐—ด๐—ด๐—ถ๐—ป๐—ด

One of the most basic yet powerful debugging techniques is the humble print statement (or console.log for JavaScript developers). By strategically placing print statements throughout your code, you can observe the values of variables, track the execution flow, and identify potential issues.

# Python example
x = 10
y = 20
print(f"Initial values: x = {x}, y = {y}")
# Some code...
print(f"After operation: x = {x}, y = {y}")

While print statements are handy, they can become cumbersome and cluttered, especially in larger codebases. This is where logging comes into play. Most programming languages provide logging libraries or frameworks that offer more advanced features, such as log levels, file output, and remote logging.

// JavaScript example
var age = 18;
console.log(age);
// or
const logger = require('winston');
logger.info('Starting application...');
// Some code...
logger.error('An error occurred:', error);

Tip : There is this handy extension in VS Code that lets you write meaningful print statements. The name of the extension is Python Quick Print ( for python developers ) and Turbo Console Log ( for javascript developers )

Turbo Console Log ( credits -Anas Chakroun )
Turbo Console Log ( credits -Anas Chakroun )
Python Quick Print ( creditsโ€Šโ€”โ€ŠAhadCove )
Python Quick Print ( credits - AhadCove )

Task: Choose a programming language youโ€™re familiar with and implement a simple program that calculates the area of a circle. Use print statements or logging to verify the input values and the final result.

๐—ง๐—ฒ๐—ฐ๐—ต๐—ป๐—ถ๐—พ๐˜‚๐—ฒ ๐Ÿฎ: ๐——๐—ฒ๐—ฏ๐˜‚๐—ด๐—ด๐—ฒ๐—ฟ๐˜€

Debuggers are powerful tools that allow you to step through your code line by line, inspect variables, set breakpoints, and even modify values on the fly. Most modern IDEs (Integrated Development Environments) and code editors come with built-in debuggers or plugins that support debuggers for various programming languages.

Itโ€™s like having a time machine for your code, allowing you to travel back and forth, observing every twist and turn. Just be careful not to accidentally create a paradox and erase your code from existence : ) .

clock moving backwards

Fun Fact: The first debugger was introduced in 1988 by Andrew Begel and Susan L. Graham for the Cornell Program Synthesizer, a programming environment for novice programmers.

In VS Code for most languages you can run the code in debug mode. Below is a quick overview :

Debug and run option
Run and Debug option

In the above picture you can see one option to run and debug your code you, open that and press โ€œRun and Debugโ€. This will run your code in debug mode. ( if it prompts you to install a debugger for particular language, install it )

If your code has no errors, it will just run normally and output will be printed. But if it has errors then the execution will automatically stop at that point and you will see details.

But what if you have a logical error? You are getting the answer without errors but its not desired. For that, there something called a breakpoint. You can add a breakpoint to any line of your code. Below is an example.

function calculateSum(numbers) {
let sum = 0;
for (let i = 0; i <= numbers.length ; i++) {
sum += numbers[i];
}
return sum;
}
// Example usage
const numbers = [5, 10, 15, 20];
const sumOfNumbers = calculateSum(numbers);
console.log(`Sum of numbers: ${sumOfNumbers}`);
// Expected: 50, but will give an incorrect result due to the logical error
// which we will see below how to solve

Here I have written a code for summing up the numbers in array but I am getting NaN as output.

Debug example image

Task: Try to find the error in the above code yourself :)

I am getting an answer but itโ€™s not desired. Letโ€™s add a breakpoint.

breakpoint image

If you hover just a little left to the line numbers you can click to add a breakpoint. I added the breakpoint on the line of initialisation of loop. Now if I run it I will see a window like this :

Live Debugging Image

The layout youโ€™re seeing is the Visual Studio Code (VSCode) debugger interface. Let me explain the different sections:

  1. Variables Section (Top Left): This section displays the variables and their values in the current scope. You can expand variables like objects or arrays to inspect their properties or elements.
  2. Code Editor (Center): This is the main code editor area where your code is displayed. The yellow highlight on line 5 indicates a breakpoint set by the debugger.
  3. Debug Console (Bottom): This console displays debug messages, error logs, and output from your program. In the image, you can see the current working directory and the script being executed (/home/darshan/.nvm/versions/node/v13.2.0/bin/node ./main.js).
  4. Call Stack (Left Center): This section shows the call stack, which represents the sequence of function calls that led to the current execution point. In the image, you can see that the calculateSum function is being executed from the main.js file.
  5. Loaded Scripts (Bottom Left): This section lists the scripts or modules that are loaded and being executed by your program.
  6. Breakpoints (Bottom Left): This section displays the breakpoints set in your code. You can manage, enable, or disable breakpoints from here.
  7. Debug Controls (Top): These are the controls for debugging, such as Start/Continue, Step Over, Step Into, Step Out, Restart, and Stop. These controls allow you to navigate through your code during the debugging process.

You will use the Debug Controll to step by step verify the code execution

Debug Controls โ€” details:

  • Continue/Pause (Green Arrow/Pause Icon): This control allows you to continue or pause the execution of your program. When paused, you can inspect variables, step through code, etc.
  • Step Over (F10): This command steps over the current line of code, executing it but not stepping into any function calls on that line.
  • Step Into (F11): This command steps into the function call on the current line, allowing you to debug the functionโ€™s code line by line.
  • Step Out (Shift+F11): If youโ€™re currently stepped into a function, this command will step out of the current function, returning to the line where the function was called.
  • Restart (Green Circle Arrow): This restarts the debugging session from the beginning of your program.
  • Stop (Red Square): This stops the current debugging session entirely.

OK now that we all are aware about the debugging environment, letโ€™s debug our code.

Debugging video example

In the video example of debugging, we noticed that the loop is running five times, but our array has only four elements (as shown in the first picture). Aha! The error lies in writing the loopโ€™s termination condition. It should be

for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
//( removed the equal to sign )

Instead of using the condition i <= numbers.length, which would cause the loop to iterate one extra time beyond the array's length, we should use i < numbers.length. This ensures that the loop terminates before attempting to access an out-of-bounds index, which would result in an undefined value being added to the sum, leading to an incorrect result.

By stepping through the code with the debugger and observing the loopโ€™s behaviour, we could identify the root cause of the issue: the loop condition was incorrect, causing it to iterate one too many times. The debugger allowed us to inspect the loop counter variable (i) and the numbers array, revealing that the loop was accessing an index beyond the array's bounds.

The ability to step through code, inspect variables, and observe the execution flow is a powerful aspect of debugging tools like the Visual Studio Code debugger. By leveraging these capabilities, developers can effectively identify and resolve subtle issues in their code, leading to more reliable and robust software applications.

Task: Set up a debugger in your preferred IDE or code editor and practice stepping through a simple program of your choice. Experiment with setting breakpoints, inspecting variables, and using other debugger features.

๐—ง๐—ฒ๐—ฐ๐—ต๐—ป๐—ถ๐—พ๐˜‚๐—ฒ ๐Ÿฏ: ๐—จ๐—ป๐—ถ๐˜ ๐—ง๐—ฒ๐˜€๐˜๐—ถ๐—ป๐—ด

Unit testing is a software development practice that involves writing small, isolated tests for individual units of code (e.g., functions, methods, or classes). These tests verify the correctness of the code by providing specific inputs and asserting the expected outputs.

Writing unit tests not only helps catch bugs early in the development process but also serves as a form of documentation and aids in refactoring and maintenance. When a test fails, it provides valuable information about the codeโ€™s behaviour, making it easier to identify and fix issues. Below code using assert is just for beginners. For advanced testing one can you libraries like jest.

// rectangle.js

function calculateArea(length, width) {
// if (length < 0 || width < 0) {
// throw new Error('Length and width must be non-negative numbers');
// }
return length * width;
}

// Unit tests

const assert = require('assert');

// Test case for valid inputs
const length = 5;
const width = 3;
const expectedArea = 15;
const actualArea = calculateArea(length, width);
try {
assert.strictEqual(actualArea, expectedArea, 'should calculate the area of a rectangle correctly');
console.log('โœ… Test passed: should calculate the area of a rectangle correctly');
} catch (err) {
console.error('โŒ Test failed: should calculate the area of a rectangle correctly');
console.error(err);
}

// Test case for zero inputs
const length2 = 0;
const width2 = 0;
const expectedArea2 = 0;
const actualArea2 = calculateArea(length2, width2);
try {
assert.strictEqual(actualArea2, expectedArea2, 'should return 0 for zero inputs');
console.log('โœ… Test passed: should return 0 for zero inputs');
} catch (err) {
console.error('โŒ Test failed: should return 0 for zero inputs');
console.error(err);
}

// Test case for negative inputs (intentionally failing)
const length3 = -5;
const width3 = 3;
try {
assert.throws(() => calculateArea(length3, width3), Error, 'Length and width must be non-negative numbers', 'should throw an error for negative inputs');
console.log('โœ… Test passed: should throw an error for negative inputs');
} catch (err) {
console.error('โŒ Test failed: should throw an error for negative inputs');
console.error(err);
}
Output image for unit testing code.

In this example, the code block that checks for non-negative length and width inputs in the calculateArea function is intentionally commented out. As a result, the third test case that expects an error to be thrown for negative inputs fails.

The output shows the first two test cases passing with a โ€œโœ…โ€ emoji, but the third test case fails with a โ€œโŒโ€ emoji and an error message indicating that the expected error was not thrown. ( emojis are written for better readability )

This scenario demonstrates how a simple code change (commenting out a crucial condition check) can lead to failing test cases, emphasizing the importance of comprehensive testing and code reviews.

While the provided code is a good starting point for beginners learning about unit testing and debugging in JavaScript, itโ€™s essential to note that the assert module from Node.js is a basic tool for writing simple assertions and tests. As your codebase and testing requirements grow more complex, it's recommended to use more advanced testing frameworks like Jest or Mocha.

For example with Jest, what you can do is make 2 seperate files rectangle.js and rectangle.test.js. Run npm init (in the test entry, you can just enter jest), install Jest by npm i jest.

// rectangle.js
function calculateArea(length, width) {
// if (length < 0 || width < 0) {
// throw new Error('Length and width must be non-negative numbers');
// }
return length * width;
}
module.exports = calculateArea;
// rectangle.test.js
const calculateArea = require('./rectangle');

describe('calculateArea', () => {
it('should calculate the area of a rectangle correctly', () => {
const length = 5;
const width = 3;
const expectedArea = 15;
const actualArea = calculateArea(length, width);
expect(actualArea).toBe(expectedArea);
});

it('should return 0 for zero inputs', () => {
const length = 0;
const width = 0;
const expectedArea = 0;
const actualArea = calculateArea(length, width);
expect(actualArea).toBe(expectedArea);
});

it('should throw an error for negative inputs', () => {
const length = -5;
const width = 3;
expect(() => calculateArea(length, width)).toThrowError(
'Length and width must be non-negative numbers'
);
});
});

Then, run npm test in the terminal.

Jest test output

As you can see similar output is obtained. Letโ€™s say we uncomment the essential lines for the negative input received and then run npm test again

jest output all test cases passed

As expected all test cases are passed.

These frameworks provide additional features, better test organization, and more robust reporting capabilities, making it easier to write, maintain, and scale your tests as your project evolves. They also offer features like test runners, code coverage reports, mocking, and parallel test execution, which can significantly improve the developer experience and the quality of your tests.

Task: Implement a simple function (e.g., calculate the factorial of a number, find the maximum value in a list, or reverse a string) and write unit tests to cover different scenarios, including edge cases and error handling.

๐—ง๐—ฒ๐—ฐ๐—ต๐—ป๐—ถ๐—พ๐˜‚๐—ฒ ๐Ÿฐ: ๐—–๐—ผ๐—ฑ๐—ฒ ๐—ฅ๐—ฒ๐˜ƒ๐—ถ๐—ฒ๐˜„ ๐—ฎ๐—ป๐—ฑ ๐—ฃ๐—ฎ๐—ถ๐—ฟ ๐—ฃ๐—ฟ๐—ผ๐—ด๐—ฟ๐—ฎ๐—บ๐—บ๐—ถ๐—ป๐—ด

Collaboration is key when it comes to debugging and troubleshooting. Code reviews and pair programming are two practices that can significantly improve code quality and help identify potential issues before they become more significant problems.

Code reviews involve having one or more developers review the code written by another developer. This fresh perspective can often uncover issues, inconsistencies, or areas for improvement that the original developer may have missed.

Pair programming, on the other hand, involves two developers working together on the same code simultaneously. One developer (the driver) writes the code while the other (the navigator) reviews the code, suggests improvements, and helps identify potential issues.

These collaborative techniques not only aid in debugging but also promote knowledge-sharing, encourage best practices, and improve overall team communication and productivity.

Fun Fact: According to a study by SmartBear Software, peer code reviews can catch up to 60% of defects that would otherwise go undetected.

Task: Find a coding partner or a group of developers interested in pair programming or code reviews. Collaborate on a small project or code exercise, and observe how the different perspectives and insights can help identify and resolve issues more effectively.

๐—ช๐—ฟ๐—ฎ๐—ฝ๐—ฝ๐—ถ๐—ป๐—ด ๐—จ๐—ฝ

Debugging and troubleshooting are essential skills for any developer, regardless of experience level or domain. By mastering the techniques weโ€™ve discussed, such as print statements and logging, debuggers, unit testing, code reviews, and pair programming, youโ€™ll be better equipped to identify and resolve issues efficiently.

โ€œDebugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.โ€ โ€” Brian Kernighan

Remember, debugging is not just about fixing bugs; itโ€™s also an opportunity to learn and improve your coding skills. Embrace the challenges, stay curious, and donโ€™t be afraid to ask for help when you need it. With practice and perseverance, youโ€™ll become a true debugging ninja, capable of tackling even the most complex issues with ease.

Happy debugging!

--

--