Debugging JavaScript
Find bugs with console output, stack traces, the debugger statement and the Node inspector.
What you will learn
- Read a stack trace from the top
- Use console methods to inspect values while a program runs
- Pause a program with the debugger statement and the Node inspector
- Work through a bug by writing a hypothesis and testing it
On this page
A bug is a mismatch between what you expected and what happened. The fundamentals lesson on debugging describes the method: reproduce, form a hypothesis, test it, fix, and check. This lesson shows the JavaScript tools for each step.
Read the error first
When a program stops with an uncaught error, Node prints the location, the message and a stack trace. A stack trace lists the calls that were active when the error was created, innermost first.
The block below is what Node 26 prints for a small program in a made-up project folder, /home/ada/shop/total.mjs. In it, outer and inner are functions, which the next module teaches. All you need now is that outer called inner, and inner threw.
file:///home/ada/shop/total.mjs:2 throw new Error("something broke"); ^
Error: something broke at inner (file:///home/ada/shop/total.mjs:2:9) at outer (file:///home/ada/shop/total.mjs:5:3) at file:///home/ada/shop/total.mjs:7:1 at ModuleJob.run (node:internal/modules/esm/module_job:569:25) at async node:internal/modules/esm/loader:650:26 at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:105:5)
Node.js v26.11.1Read it from the top. The first lines say what happened and where. The at lines then say who called whom. The top at line is where the error was raised, the next is the caller, and so on, down to the start of the program. Each ends in file:line:column, which most terminals and editors let you click. Start at the top at line that is in your code. Lines inside node: modules are Node’s internals.
You can also reach the trace from code. The stack property is a string whose first line is name: message, followed by the at lines.
try { null.length;} catch (error) { const lines = error.stack.split("\n"); console.log(lines[0]); console.log(lines[1].trim().startsWith("at "));}TypeError: Cannot read properties of null (reading 'length')truesplit("\n") cuts the text into lines, and lines[0] is the first one. The stack text is not standardised in detail and differs between engines, so use it for reading, not for parsing.
Looking at values with console
The quickest debugging tool is printing. The aim is to see a value at the point where you suspect it goes wrong.
const orders = [ { id: 1, item: "pen", qty: 3, price: 1.5 }, { id: 2, item: "book", qty: 1, price: 12 },];
let total = 0;for (const order of orders) { console.log("order:", order); total += order.qty * order.price;}
console.log({ total });console.table(orders);console.group("details");console.log("inside the group");console.groupEnd();console.count("check");console.count("check");order: { id: 1, item: 'pen', qty: 3, price: 1.5 }order: { id: 2, item: 'book', qty: 1, price: 12 }{ total: 16.5 }┌─────────┬────┬────────┬─────┬───────┐│ (index) │ id │ item │ qty │ price │├─────────┼────┼────────┼─────┼───────┤│ 0 │ 1 │ 'pen' │ 3 │ 1.5 ││ 1 │ 2 │ 'book' │ 1 │ 12 │└─────────┴────┴────────┴─────┴───────┘details inside the groupcheck: 1check: 2console.log("order:", order)puts a label in front of the value. Labelled output is far easier to scan than a column of bare numbers.console.log({ total })is a shortcut: the braces create an object whose key is the variable name, so the output shows{ total: 16.5 }and you do not need to type the label.console.tablelays an array of objects out as a table.console.groupindents the output untilconsole.groupEnd.console.countcounts how many times a line was reached, which answers “does this code even run?”console.assert(condition, ...message)prints a message only when the condition is false. It goes to the error stream, as doconsole.errorandconsole.warn, so you can separate those from normal output.
Remove or silence the printing when you are done. Debugging output that stays in the code becomes noise.
Pausing with debugger
Printing shows you values at the points you chose. A debugger lets you stop the program and look at everything, then step forward one line at a time.
The statement debugger; marks a place to pause. It does nothing unless a debugger is attached.
const numbers = [2, 4, 9];let sum = 0;
for (const n of numbers) { sum += n;}
debugger; // pauses here only when a debugger is attachedconsole.log(sum / numbers.length);5Run normally, the program just prints the average, 5. Run under the inspector, it stops on the debugger line. Node offers two ways to start it.
node --inspect-brk debugger_stmt.mjsstarts the program, waits for a debugger client, and pauses on the first line. Openchrome://inspectin Chrome (edge://inspectin Edge) and click “inspect” under your Node process. (--inspectstarts the inspector without pausing at the first line, which is what you want for a program that keeps running.)- In VS Code, use the Run and Debug panel to start the file under its built-in Node debugger. A click in the margin next to a line number sets a breakpoint, which pauses at that line without editing the code. Chrome DevTools offers breakpoints the same way.
There is also a plain command-line debugger: node inspect debugger_stmt.mjs.
Once paused you can:
- read the current value of any variable in scope,
- step over a line, step into a function call, or step out of the current function,
- watch an expression as it changes,
- see the call stack, the same list as in a stack trace but live, and click a caller to see its variables.
The browser’s DevTools offer the same features for code running in a page, under the “Sources” panel.
A debugging routine
When something is wrong:
- Reproduce it with the smallest input you can.
- Read the error if there is one, and go to the first line in your code.
- Write a hypothesis in one sentence, such as “the loop runs one time too many”. It must be something you can test.
- Test it with a print or a breakpoint. Do not change code yet.
- Fix one thing, run again, and check that the symptom is gone and nothing else broke.
Writing the hypothesis down matters. It stops you changing code at random and gives you something to cross out when you are wrong.
Common pitfalls
- Changing code before understanding the bug. Several guesses at once leave you unsure which one worked.
- Ignoring the first line of the error. The message usually names the exact problem.
- Logging an object that you later change. The console may show the object as it is when you expand it, not when it was printed. Log a copy, or a specific field.
- Leaving
debugger;andconsole.login finished code. Remove them or use a proper logging tool. - Debugging the wrong file. Check that the code you edit is the code that runs.
Exercises
Four small programs follow, each with one bug. Run each one. For each wrong output write down a hypothesis, test it with console.log or a breakpoint, and then fix the code.
Bug 1. This should print the average of the numbers.
const numbers = [2, 4, 9];let sum = 0;for (let i = 0; i <= numbers.length; i++) { sum += numbers[i];}console.log(sum / numbers.length);Bug 2. The three form fields are text. This should print their sum.
const fields = ["10", "20", "30"];let total = 0;for (const field of fields) { total = total + field;}console.log(total);Bug 3. This should print empty only for an empty list. The list holds two items.
const list = [1, 2];if (list.length = 0) { console.log("empty");} else { console.log("not empty");}console.log(list);Bug 4. This should print the price doubled.
const price = 4;price * 2;console.log(price);Input: none. The buggy programs print NaN, then 0102030, then not empty followed by [], then 4. Expected output after the fixes: 5, 60, not empty followed by [ 1, 2 ], and 8.
Show solution
- The average prints
NaN. Hypothesis: the loop reads past the end of the array, because<=runs one pass too many andnumbers[3]isundefined. Addingundefinedto a number givesNaN. Aconsole.log(i, numbers[i])inside the loop confirms it. Fix: use<.
const numbers = [2, 4, 9];let sum = 0;for (let i = 0; i < numbers.length; i++) { sum += numbers[i];}console.log(sum / numbers.length);5- The total prints
0102030. Hypothesis: the form fields are strings, so+joins text instead of adding. The coercion lesson covers this. Fix: convert withNumber(field).
const fields = ["10", "20", "30"];let total = 0;for (const field of fields) { total = total + Number(field);}console.log(total);60- The list is reported as not empty, but it is now
[]. Hypothesis:list.length = 0assigns instead of comparing, which clears the list and gives a falsy result. The second value printed,[], gives it away. Fix:list.length === 0.
const list = [1, 2];if (list.length === 0) { console.log("empty");} else { console.log("not empty");}console.log(list);not empty[ 1, 2 ]- The price prints
4. Hypothesis:price * 2;computes a value and throws it away, because nothing stores or prints it. Fix: store the result and print that.
const price = 4;const doubled = price * 2;console.log(doubled);8Given const prices = [3, 8, 5];, use a for...of loop and console.log to show the running total after each price, in the form price: 3 total: 3. After the loop, print the final total with the { total } shortcut.
Input: none. Expected output: price: 3 total: 3, price: 8 total: 11, price: 5 total: 16, { total: 16 }.
Show solution
const prices = [3, 8, 5];let total = 0;
for (const price of prices) { total += price; console.log("price:", price, "total:", total);}
console.log({ total });price: 3 total: 3price: 8 total: 11price: 5 total: 16{ total: 16 }Printing inside the loop shows every step, so a wrong value would show up at the pass where it first appears. The { total } line labels the final value with its variable name.
This program should print every item, but it skips the first.
const items = ["a", "b", "c"];for (let i = 1; i < items.length; i++) { console.log(items[i]);}Write your hypothesis in one sentence. Test it by printing i next to each item, then fix the program.
Input: none. The buggy program prints b and c. Expected output after the fix: a, b, c.
Show solution
Hypothesis: the counter starts at 1, but the first item sits at position 0. Printing i next to each item shows the loop starts at 1.
const items = ["a", "b", "c"];for (let i = 0; i < items.length; i++) { console.log(items[i]);}abcCounting starts at zero, so the loop must too. Writing the hypothesis first meant one change, not a guess at the loop condition.
Summary
- Read errors from the top: the message first, then the first
atline in your own code. - Label what you print, and use
console.table,console.groupandconsole.countto see more at once. debugger;and breakpoints pause the program. Start Node withnode --inspect-brk file.mjsand attach Chrome DevTools or VS Code to step through it.- Debug with a routine: reproduce, hypothesise, test, fix one thing, check.
Next module: functions, starting with the function forms lesson.