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 TypeWhen It OccursExample
SyntaxErrorCode cannot be parsed — caught before runningMissing closing bracket
ReferenceErrorAccessing an undefined variableconsole.log(x) where x is not declared
TypeErrorWrong type used in an operationnull.toUpperCase()
RangeErrorValue outside valid rangenew Array(-1)
Logic ErrorCode runs but produces wrong resultUsing + 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); // 15

console.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 null

11. 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 typeof or console.log()
  • Check for typos in variable and function names (case-sensitive)
  • Ensure async operations are properly awaited
  • Use debugger or 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(), and console.time() for quick debugging
  • The debugger statement 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

Leave a Comment

Your email address will not be published. Required fields are marked *