Mastering the Art of Debugging: Techniques for Efficient Troubleshooting ๐ฏ
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
- Technique 1: Print Statements and Logging
- Technique 2: Debuggers
- Technique 3: Unit Testing
- Technique 4: Code Review and Pair Programming
- 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 )
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 : ) .
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 :
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.
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.
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 :
The layout youโre seeing is the Visual Studio Code (VSCode) debugger interface. Let me explain the different sections:
- 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.
- 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.
- 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
). - 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 themain.js
file. - Loaded Scripts (Bottom Left): This section lists the scripts or modules that are loaded and being executed by your program.
- Breakpoints (Bottom Left): This section displays the breakpoints set in your code. You can manage, enable, or disable breakpoints from here.
- 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.
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);
}
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.
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
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!