Every programmer writes bugs. What separates beginners from experienced developers is how quickly they find them. This lesson replaces guessing with a method: read the error, form a theory, then confirm it with logs or by pausing the code and looking inside. The examples use the quiz app and Chrome DevTools; Firefox and Safari have the same tools under slightly different names.
Open DevTools
Press F12, or Ctrl+Shift+I on Windows and Linux, Cmd+Option+I on macOS. The panels you need are:
- Console: errors, warnings and your logs, plus a place to run JavaScript against the page.
- Sources (called Debugger in Firefox): your files, breakpoints and step controls.
- Elements (Inspector in Firefox): the live DOM.
Keep the Console open while you develop. Many bugs announce themselves there, and nobody sees them if it is closed.
Reading an error message
Break the quiz on purpose. In the Next button handler from lesson 16, change < to <=:
if (currentIndex <= questions.length) {
showQuestion(currentIndex);
questionText.focus();
} else {
showResults();
}
Play through all three questions and press Next at the end. Nothing happens on the page, but the Console shows something like this:
Uncaught TypeError: Cannot read properties of undefined (reading 'text')
at showQuestion (quiz.js:31:42)
at HTMLButtonElement.<anonymous> (quiz.js:94:5)
The line and column numbers depend on your file. Read the message in three parts:
- The error type:
TypeError. The common ones areReferenceError(a name that does not exist, often a typo),TypeError(using a value in a way its type does not allow, such as reading a property ofundefined) andSyntaxError(code that cannot be parsed, so nothing in the file runs). - The message: it was trying to read
textfrom something that wasundefined. So the question is not "why is text missing?" but "why is the thing before.textundefined?". - The location:
quiz.js, line 31, column 42. Click it to jump to that exact spot in the Sources panel.
Firefox words the same error as TypeError: can't access property "text", question is undefined. Different words, same facts.
Reading the stack trace
The indented lines under the message are the stack trace: the chain of function calls that led to the error, newest first.
at showQuestion (quiz.js:31:42): the error happened here, inshowQuestion.at HTMLButtonElement.<anonymous> (quiz.js:94:5):showQuestionwas called from an anonymous function running for a button, the Next button's click listener.
Read it top to bottom: where it broke, then who called it, and who called that. The bug is often not on the top line. Here line 31 (question.text) is innocent; it received a bad index. The real mistake is in the caller, which let currentIndex reach 3 when the valid indexes are 0 to 2. Following the trace down is how you find that.
Named functions make traces easier to read than <anonymous>, which is a good reason to name your important handlers.
Logging with purpose
console.log is the quickest way to test a theory. Log a labelled object rather than a bare value, so you can tell your logs apart:
console.log({ currentIndex, total: questions.length });
// → { currentIndex: 3, total: 3 }
The shorthand { currentIndex } creates an object whose key is the variable's name. The console has more than log:
| Method | Use it for |
|---|---|
console.warn(), console.error() | Messages that stand out, with a stack trace you can expand |
console.table(questions) | Arrays of objects, shown as a sortable table |
console.group("Answer") and console.groupEnd() | Grouping related logs so they can be collapsed |
console.count("click") | Counting how often a line runs |
console.assert(score <= questions.length, "Score too high") | Logging only when a condition is false |
console.time("render") and console.timeEnd("render") | Measuring how long something takes |
console.dir(element) | Showing a DOM element as an object with all its properties |
A logged object is shown as it is when you expand it, not when it was logged. If it changes afterwards, the expanded view can mislead you. Log JSON.stringify(value) or a copy when the moment matters.
Remove debugging logs when you are done. They clutter the console and can expose data.
Pausing with breakpoints
Logging needs you to guess in advance what to print. A breakpoint pauses the code at a line so you can look at everything: every variable, the call stack and the page as it is at that moment.
Fix the <= back to <, then introduce a quieter bug: in the questions array, change the first answer to "const ", with a trailing space. Now a correct answer to question 1 is marked wrong, and there is no error at all. Logic bugs like this are where breakpoints shine.
- Open Sources and select
quiz.jsfrom the file tree (Ctrl+P or Cmd+P opens a file by name). - Click the line number of
const isCorrect = button.textContent === question.answer;inhandleAnswer. A blue marker appears. - Choose
constin the quiz. The page freezes and the line is highlighted. That line has not run yet.
While paused, look at the right-hand side:
- Scope lists every variable visible at this point: local ones such as
buttonandquestion, then outer scopes, then globals. - Call Stack is the live version of a stack trace. Click a frame to see that function's variables.
- Watch lets you add expressions that are evaluated at every pause. Add
button.textContentandquestion.answer.
You can also hover over a variable in the code, or type expressions into the Console, which runs in the paused scope:
JSON.stringify(button.textContent)
// → "\"const\""
JSON.stringify(question.answer)
// → "\"const \""
JSON.stringify makes invisible differences visible: there is the stray space. Remove it from the array and the bug is gone.
Stepping through code
The toolbar above the Scope pane controls execution:
| Button | Shortcut | What it does |
|---|---|---|
| Resume | F8 | Run until the next breakpoint |
| Step over | F10 | Run the current line, including any function it calls, and pause on the next line |
| Step into | F11 | Enter the function called on this line |
| Step out | Shift+F11 | Finish the current function and pause in its caller |
Step over the isCorrect line and the Scope pane shows its new value. Step into markAnswer(button, isCorrect) to follow the call. Stepping one line at a time while watching the variables is the most reliable way to find where reality parts from your expectations.
Smarter breakpoints
Right-click a line number for more options:
- Conditional breakpoint: pause only when an expression is true, for example
currentIndex === 2. Ideal inside loops and handlers that run many times. - Logpoint: log a message without pausing and without editing your file, for example
"answer", question.answer.
The Breakpoints sidebar also offers:
- Event listener breakpoints: pause on any
click,keydownorsubmit, which helps when you do not know which code handles an event. - Pause on uncaught exceptions: stop at the moment an error is thrown, with all the variables still available. Turn this on and repeat the
<=bug: you will be paused on the failing line withquestionshowing asundefinedandindexas3.
In the Elements panel, right-click an element and choose Break on then subtree modifications or attribute modifications to find which code changes it.
The debugger statement
Writing debugger; in your code pauses execution at that line whenever DevTools is open. It does nothing when DevTools is closed. It is handy when a file is hard to find in Sources, but delete it before committing.
function handleAnswer(event) {
debugger;
// ...
}
Debugging in Node.js
The same tools work for scripts run with Node.js. Run node --inspect-brk script.js, then open chrome://inspect in Chrome and click inspect under your script. You get the Sources panel, breakpoints and stepping. Editors such as VS Code have a built-in debugger that does the same from the editor.
A debugging method
- Reproduce the bug reliably, with the smallest steps you can.
- Read the error type, message and stack trace in full.
- Form a theory about which value is wrong and where.
- Check it with a log or a breakpoint. Do not change code until you have evidence.
- Fix the cause, not the symptom, then repeat the steps to confirm.
Summary
- An error message has a type, a description and a location; click the location to jump to the code.
- A stack trace lists the calls that led to the error, newest first, and the real mistake is often further down.
- Log labelled objects, and use
table,group,count,assertandtimewhere they fit. - Breakpoints pause code so you can inspect the Scope, Call Stack and Watch panes, then step over, into or out.
- Use conditional breakpoints, logpoints, event listener breakpoints and pause on exceptions to find bugs faster.