Debugging Is a Method, Not a Talent
Debugging is a repeatable process rather than an instinct some people are born with. The process is: reproduce the problem reliably, form one specific guess about the cause, test that guess, and repeat. Most of the time lost to debugging is lost by skipping the first step and changing code in the hope that something improves.
Reproducing reliably matters more than anything else. A bug you can trigger on demand is most of the way to being solved; a bug that happens "sometimes" is a research project. Write down the exact steps, the exact input and the exact browser. Very often the act of writing it down reveals the one condition you had been varying at random without noticing.
Then form a guess you can actually check. "The form is broken" cannot be tested. "The email value is arriving as a string of spaces rather than empty" can be, in about ten seconds. A specific guess turns a vague complaint into a yes-or-no question, and every yes-or-no answer removes half the remaining possibilities.
Change one thing at a time, and put it back when it does not help. A file full of half-finished experimental edits is how a one-hour bug becomes a one-day bug, because you can no longer tell which change caused which symptom. If you find yourself in that state, commit or stash what you have and start again from something clean.
// A guess you cannot test: "the form is broken"
// Guesses you can test in ten seconds each:
const value = ' ';
console.log('value:', JSON.stringify(value)); // '" "' - spaces, not empty
const marks = '82';
console.log('type:', typeof marks, 'value:', marks); // 'string' '82'
function handleSubmit() {
console.log('handler ran'); // did it run at all?
}
handleSubmit(); - "It works on my machine" is usually a reproduction problem, not a mystery. Different data, a different browser, a slower connection or a cached old file explain the large majority of these.
console, Beyond log
console.log is the right tool far more often than people like to admit. The console also has several other methods that answer specific questions much faster than a log would.
console.table(arrayOfObjects) prints a real table with sortable columns, which beats scrolling through twenty logged objects looking for the odd one out. console.dir(element) shows a DOM element as an object with all its properties, where console.log shows it as markup. console.count(label) reports how many times a line has run, which answers "is this handler firing twice?" without you adding a counter variable.
console.time(label) with console.timeEnd(label) measures how long something took. console.group and console.groupEnd nest related output so that logs from inside a loop stay readable instead of flooding the panel. console.assert(condition, message) prints only when the condition is false, which is a compact way of stating an assumption you want checked but do not want to read about when it holds.
Two habits make logging dramatically more useful. Label every log with what it is — console.log('marks after conversion:', marks) rather than a bare value in a wall of numbers you can no longer attribute. And print with JSON.stringify whenever whitespace or type might matter, because console.log(' ') and console.log('') look identical on screen while JSON.stringify shows the difference at once.
const students = [
{ name: 'Ananya', marks: 82, grade: 'A' },
{ name: 'Rahul', marks: 68, grade: 'B' },
{ name: 'Priya', marks: 91, grade: 'A' }
];
console.table(students); // a real table with sortable columns
console.group('Processing students');
students.forEach(s => console.log(s.name, s.marks));
console.groupEnd();
console.time('sum');
const total = students.reduce((t, s) => t + s.marks, 0);
console.timeEnd('sum'); // 'sum: 0.123ms'
console.assert(total > 0, 'total should be positive'); // silent when true
// Is this running more often than I think?
function onClick() {
console.count('onClick'); // 'onClick: 1', 'onClick: 2', ...
}
onClick();
onClick();
// Whitespace is invisible without stringify
const typed = ' ';
console.log(typed); // looks empty
console.log(JSON.stringify(typed)); // '" "' - two spaces console.logof an object shows a live reference in some browsers, so expanding it later can show values from later in the program rather than from the moment you logged it. When that matters, logJSON.stringify(obj)or a spread copy instead.
Reading an Error Message Properly
Most people glance at an error and immediately start guessing. Reading it properly takes about fifteen seconds and very often hands you the answer outright.
Every error message has three parts. The type gives you the category — TypeError, ReferenceError, SyntaxError. The message says what specifically went wrong. The stack trace says where, with the line that threw at the top and the chain of callers beneath it.
Three messages account for most of what you will ever see. Cannot read properties of undefined (reading 'x') means something you expected to be an object is undefined — and the crucial detail is that the real problem is one step before the line that threw. x is not a function means a typo, a wrong import, or a value that is not the type you assumed. x is not defined means a misspelt name, a missing import, or use before declaration.
The line number in a trace is where the error was detected, which is not always where it was caused. For "cannot read properties of undefined", the useful question is why that value was undefined, and the answer lives further up. Read the stack downwards until you reach the first line that is your own code rather than a library's, and start the investigation there.
const students = [{ name: 'Ananya' }];
// TypeError: Cannot read properties of undefined (reading 'name')
// console.log(students[5].name);
// The throwing line is not the problem. students[5] being undefined is.
console.log(students[5]); // undefined - THIS is the real finding
console.log(students[5]?.name); // undefined, and no crash
console.log(students.length); // 1 - and now the cause is obvious
// TypeError: ... is not a function - a typo, or the wrong type
const name = 'Ananya';
// name.toUpperCse(); // typo
// students.toUpperCase(); // an array has no such method
// ReferenceError: ... is not defined
// console.log(totalMarks); // never declared, or misspelt - Errors coming from inside a library are usually still your bug. The library threw because of what it was handed, so read the stack down to the last line of your own code and check the arguments you passed from there.
Breakpoints and the Debugger
A log tells you one value at one moment. A breakpoint pauses execution and lets you inspect everything at that moment — every variable in scope, the entire call stack, and the current state of the page. For anything more involved than a two-line question, it is considerably faster than adding logs and reloading.
Open the Sources panel of the developer tools, find your file, and click a line number to set a breakpoint. Reload or trigger the code and execution stops there. The Scope panel lists every variable currently in scope, the Call Stack panel shows exactly how you arrived at that line, and the Watch panel evaluates any expression you type as you step through.
Four controls do most of the work. Step over runs the next line without going inside it. Step into enters the function on the current line. Step out finishes the current function and returns to its caller. Resume continues until the next breakpoint. You can also write debugger; directly in your code, which pauses exactly like a breakpoint when the developer tools are open and does nothing at all when they are not.
Two kinds of breakpoint are worth knowing about. A conditional breakpoint — set by right-clicking a line number — pauses only when an expression you supply is true, which is how you stop on the one iteration of a loop that matters rather than clicking Resume two hundred times. And "pause on exceptions" stops automatically wherever an error is thrown, which is by far the quickest way to locate a failure buried inside a library or deep in a promise chain.
function calculateTotal(items) {
let total = 0;
for (const item of items) {
debugger; // pauses here when devtools is open
total += item.price * item.qty;
}
return total;
}
console.log(calculateTotal([
{ price: 60, qty: 2 },
{ price: 15, qty: 3 }
])); // 165
// A conditional breakpoint set on the line above, with the condition
// item.qty > 2
// pauses only on the second item, and skips the first entirely. - Remove every
debugger;statement before you commit. Left in, it pauses the page for any developer with the tools open — and it will be found by a colleague at the least convenient moment.
Narrowing a Bug Down
When you genuinely have no idea where a problem is, bisection finds it fast. Comment out half the code. If the problem persists, it lives in the half that remains; if it disappears, it lives in the half you removed. Repeat on whichever half is guilty, and you can find any bug in a hundred lines in about seven steps regardless of how unfamiliar the code is.
The same idea applies to data. If a function misbehaves on a list of a thousand records, try it on the first ten, then on one. A bug that appears only with one specific record tells you the problem is about the data rather than the logic — which is a completely different and usually much shorter investigation.
And it applies to time. If something used to work, the change that broke it is somewhere in your history. git bisect automates exactly this halving across commits, and even doing it by hand — checking out a commit from last week to see whether the bug is present — is far faster than reading everything that changed.
The demo below is a small function carrying two deliberate bugs of the kinds this course has already covered. One of them throws loudly, and the other is silent and produces a plausible-looking wrong answer, which is the more dangerous kind. Open the console, work through it with console.table and a breakpoint, and fix them before reading the answers at the bottom of the file.
HTML
<div class="dbg-demo">
<h3>Marks summary</h3>
<button id="run">Run</button>
<pre id="out">Press Run, then open the console with F12.</pre>
</div> CSS
.dbg-demo { padding: 20px; background: #f0f0f0; border-radius: 8px; font-family: system-ui, sans-serif; }
button { padding: 10px 16px; background: #d1039e; color: white; border: none; border-radius: 5px; cursor: pointer; }
pre { background: white; padding: 14px; border-radius: 5px; border-left: 4px solid #d1039e; margin-top: 12px; white-space: pre-wrap; font-size: 0.9rem; } JavaScript
const rows = [
{ name: 'Ananya', marks: '82' },
{ name: 'Rahul', marks: '68' },
{ name: 'Priya', marks: '91' }
];
// Two bugs are hiding in here. Use console.table, a labelled
// console.log and a breakpoint to find them before reading the
// answers at the bottom of this file.
function summarise(list) {
let total = 0;
for (let i = 0; i <= list.length; i++) { // BUG 1 is on this line
total += list[i].marks; // BUG 2 is on this line
}
const average = total / list.length;
return 'Total: ' + total + ', average: ' + average.toFixed(1);
}
document.getElementById('run').addEventListener('click', function () {
const out = document.getElementById('out');
console.table(rows);
try {
out.textContent = summarise(rows);
} catch (err) {
out.textContent = 'It threw: ' + err.message;
console.error(err); // read the stack trace, top line first
}
});
// ---------------------------------------------------------------
// ANSWERS - try it yourself first.
//
// BUG 1 i <= list.length runs one pass too many, so list[i] is
// undefined on the last pass and reading .marks throws a
// TypeError. Loud, and easy to find from the stack trace.
// Fix: i < list.length
//
// BUG 2 marks are STRINGS, so += joins text instead of adding.
// With bug 1 fixed, total becomes '0826891' and the average
// comes out as 275630.3 - no error at all, just a wrong
// number on the screen. This is the dangerous kind.
// Fix: total += Number(list[i].marks);
//
// Both fixed, the answer is: Total: 241, average: 80.3 - Bug 2 is the one worth dwelling on. It throws nothing, the page looks like it worked, and the only way to catch it is to check the output against what you knew the answer should be. That is why testing with data whose result you already know is worth the two minutes it costs.
The Bugs You Will Meet Most
After enough debugging, the same handful of causes account for most of what you find. Recognising them on sight often removes the investigation entirely.
A string where a number was expected. Form values, dataset attributes, URL parameters and JSON numbers sent as text all produce this. The tell is concatenation where you expected addition, or a comparison that comes out false when it visibly looks true. Convert at the boundary and the whole class disappears.
Something was undefined one step earlier. A selector that found nothing, a property that does not exist on the object you actually received, an array index out of range, or an await you forgot. The error always appears where the value is used, never where it went missing, so look one step back before anything else.
Timing. Code ran before the element existed, or before the data arrived. Here is the useful diagnostic: if wrapping something in a setTimeout makes it work, you have a timing bug and the setTimeout is not the fix — it is the diagnosis. The real fix is defer, DOMContentLoaded, an await, or event delegation, depending on which thing was not ready.
- Reproduce first, then guess, then test — one change at a time
console.tablefor arrays of objects;console.countfor "is this firing twice?"JSON.stringifywhenever whitespace or type might matter- Read the error type, message and stack before touching any code
- "Cannot read properties of undefined" means look one step earlier
- A conditional breakpoint beats clicking Resume two hundred times
- Bisect: comment out half, then halve again
- If a
setTimeoutfixes it, the bug is timing and is still there - Run a linter — it catches a whole category of these before you run anything
- Explaining the problem out loud — to a colleague, or to nobody in particular — solves a surprising number of bugs before you finish the sentence. It works because describing the code forces you to say the assumption you never actually checked.
