JavaScript Debugging and Best Practices
Debugging is the process of finding and fixing errors in code. Writing clean, bug-free JavaScript requires both solid debugging skills and good coding habits. This topic covers the most effective debugging techniques and professional best practices that every JavaScript developer should follow.
Types of JavaScript Errors
| Error Type | When It Occurs | Example |
|---|---|---|
| SyntaxError | Code cannot be parsed — caught before running | Missing closing bracket |
| ReferenceError | Accessing an undefined variable | console.log(x) where x is not declared |
| TypeError | Wrong type used in an operation | null.toUpperCase() |
| RangeError | Value outside valid range | new Array(-1) |
| Logic Error | Code runs but produces wrong result | Using + instead of * for multiplication |
Using console Methods for Debugging
console.log() — Basic Output
let total = 0;
for (let i = 1; i <= 5; i++) {
total += i;
console.log(`After i=${i}, total=${total}`); // Track intermediate values
}
console.log("Final total:", total); // 15console.table() — Display Data in a Table
let students = [
{ name: "Ananya", score: 90 },
{ name: "Rahul", score: 75 },
{ name: "Priya", score: 88 }
];
console.table(students);console.group() — Group Related Logs
console.group("User Details");
console.log("Name: Suresh");
console.log("Age: 28");
console.log("Role: Admin");
console.groupEnd();console.time() — Measure Execution Time
console.time("loop-time");
let sum = 0;
for (let i = 0; i < 1000000; i++) sum += i;
console.timeEnd("loop-time"); // loop-time: 2.5ms (approx)console.assert() — Log Only When Condition Fails
let age = 15;
console.assert(age >= 18, "User must be 18 or older!");
// Assertion failed: User must be 18 or older!console.trace() — Print Call Stack
function inner() {
console.trace("Trace from inner:");
}
function outer() {
inner();
}
outer();
// Shows the call stack: inner ← outer ← (caller)The debugger Statement
The debugger keyword pauses code execution in the browser's developer tools at that exact line — like setting a breakpoint manually in code.
function calculateDiscount(price, percent) {
debugger; // Execution pauses here in DevTools
let discount = price * (percent / 100);
return price - discount;
}
calculateDiscount(1000, 20);When DevTools is open, execution stops at the debugger line, allowing variable inspection and step-by-step execution.
Browser DevTools Debugging
The browser's built-in DevTools (press F12) is the most powerful debugging environment:
- Sources tab — Set breakpoints by clicking line numbers; step through code
- Console tab — Run JavaScript expressions, view logs and errors
- Network tab — Inspect HTTP requests, responses, and timing
- Elements tab — Inspect and modify the live DOM
- Performance tab — Profile JavaScript execution and rendering
Breakpoint Types in Sources Tab
- Line breakpoints — Pause on a specific line
- Conditional breakpoints — Pause only when a condition is true (right-click the line number)
- Exception breakpoints — Pause when any error is thrown
JavaScript Best Practices
1. Use const and let — Avoid var
// Bad
var count = 0;
var MAX = 100;
// Good
let count = 0;
const MAX = 100;2. Always Declare Variables
// Bad — creates accidental global variable
function setup() {
appName = "eStudy247"; // No keyword!
}
// Good
function setup() {
let appName = "eStudy247";
}3. Use Strict Equality (===)
// Bad — can produce surprising results
if (score == "100") { ... }
// Good
if (score === 100) { ... }4. Handle Errors Properly
// Bad — silent failure
function loadData() {
fetch("/api/data")
.then(res => res.json());
// No error handling!
}
// Good
async function loadData() {
try {
let res = await fetch("/api/data");
if (!res.ok) throw new Error("Request failed");
return await res.json();
} catch (err) {
console.error("loadData failed:", err.message);
}
}5. Use Meaningful Variable Names
// Bad
let x = 200;
let y = 0.18;
let z = x + x * y;
// Good
let basePrice = 200;
let taxRate = 0.18;
let totalPrice = basePrice + basePrice * taxRate;6. Write Small, Focused Functions
// Bad — one function doing too much
function processOrder(items, user, payment) {
// Validates, calculates, applies discount, processes payment, sends email...
}
// Good — each function has one responsibility
function calculateTotal(items) { ... }
function applyDiscount(total, code) { ... }
function processPayment(total, payment) { ... }
function sendConfirmationEmail(user, orderId) { ... }7. Avoid Deep Nesting
// Bad — callback pyramid of doom
getData(function(data) {
processData(data, function(result) {
saveResult(result, function(saved) {
notifyUser(saved, function() {
console.log("Done!");
});
});
});
});
// Good — use async/await for flat, readable flow
async function handleFlow() {
let data = await getData();
let result = await processData(data);
let saved = await saveResult(result);
await notifyUser(saved);
console.log("Done!");
}8. Validate Function Inputs
function divide(a, b) {
if (typeof a !== "number" || typeof b !== "number") {
throw new TypeError("Both arguments must be numbers.");
}
if (b === 0) {
throw new Error("Division by zero is not allowed.");
}
return a / b;
}9. Comment Wisely
// Bad — explains what the code does (which is obvious)
let x = x + 1; // Increment x by 1
// Good — explains WHY it is done
let retries = retries + 1; // Retry limit increased after user-reported timeout issues
// Good — explains complex logic
// Tax rate doubles for items over ₹5000 per government regulation
let tax = price > 5000 ? price * 0.28 : price * 0.12;10. Use Default Parameters and Optional Chaining
// Default parameters prevent undefined errors
function createUser(name = "Guest", role = "viewer") {
return { name, role };
}
// Optional chaining prevents TypeError on nested access
let city = user?.address?.city; // undefined instead of TypeError if any part is null11. Avoid Mutating Function Parameters
// Bad — modifies the original array passed in
function addTax(prices) {
prices.push(prices.length * 1.18); // Mutates original!
}
// Good — work on a copy
function addTax(prices) {
return [...prices, prices.length * 1.18];
}Common Debugging Checklist
- Check the browser console for error messages first
- Use
console.log()to trace variable values at each step - Verify the type of a value with
typeoforconsole.log() - Check for typos in variable and function names (case-sensitive)
- Ensure async operations are properly awaited
- Use
debuggeror breakpoints to pause and inspect execution - Check network requests in the Network tab for API issues
- Read the full error message — line numbers are shown in the stack trace
Key Points to Remember
- Use
console.log(),console.table(), andconsole.time()for quick debugging - The
debuggerstatement pauses execution at that line in browser DevTools - Set breakpoints in the Sources tab to step through code line by line
- Always use
const/let, validate inputs, and handle errors in async code - Write small, single-purpose functions with descriptive names
- Avoid deep callback nesting — use async/await for clarity
- Write comments that explain WHY — not WHAT (the code shows what)
- Use optional chaining (
?.) and nullish coalescing (??) to handle missing data gracefully
