Historical Context & Motivation
Every programmer, from beginners to seasoned professionals, encounters bugs — errors in code that cause unexpected behavior. The word "bug" has been used in engineering since the 1800s, but it became famous in computing when a moth was found stuck in a relay of the Harvard Mark II computer in 1947. Since those early days, developers have needed systematic ways to track down and fix these errors, which is exactly what debugging tools are designed to do.
In the earliest days of programming, debugging meant physically examining hardware or reading through printed output line by line. As software grew more complex, programmers realized they needed better tools to peer inside a running program. Two major approaches emerged: printing values to an output screen (what we now call console logging) and using interactive debuggers that let you pause execution and examine your program's state step by step.
Today, the question is not whether debugging tools exist — they are built into every modern browser. The real question is: how do you use these tools effectively to find and understand bugs in your own code? This lesson will teach you exactly that.
Core Principles of Debugging
Debugging is fundamentally about answering one question: what is my program actually doing versus what I expected it to do? To answer that question, you need to inspect program state — the current values of all variables and the current position of execution — at different moments while your code runs. Every debugging technique boils down to this core idea.
Program State
Console Logging
console.log() statements to print variable values and messages to the browser console. This is the simplest and most common debugging technique in JavaScript.Breakpoints
Stepping Through Code
Isolate, Hypothesize, Test
Visual Explanation — Console Logging Flow
The following diagram shows how console.log() works during program execution. When your JavaScript code runs, the engine processes each line in order. When it hits a console.log() call, it evaluates the expression inside the parentheses and prints the result to the browser console — without stopping the program. The code keeps running normally. This lets you see a "trail" of values as your program executes.
console.log() calls highlighted in cyan. The right panel shows the browser console output. Each arrow connects a log statement to its printed result, allowing you to trace how the score variable changes over time.Notice that the program runs all the way through without stopping. Console logging is a passive debugging technique — you add print statements before running your code, then read the output afterward to piece together what happened. It is fast to set up and works in any environment, but for complex bugs you may need something more interactive.
How the Browser Debugger Works
While console logging prints values passively, a browser debugger gives you active control over your program's execution. Every modern browser — Chrome, Firefox, Safari, Edge — has built-in Developer Tools (often called DevTools) that include a Sources panel where you can set breakpoints, pause execution, and step through your code line by line.
Setting a Breakpoint
To set a breakpoint, you open DevTools (press F12 or Ctrl+Shift+I on Windows, Cmd+Option+I on Mac), navigate to the Sources tab, find your JavaScript file, and click on the line number where you want execution to pause. A colored marker appears on that line. The next time your code runs and reaches that line, the JavaScript engine freezes the entire program right there, before executing that line. You can then see the value of every variable that exists at that moment.
Stepping Controls
Once paused, the debugger gives you several buttons to control what happens next. These are the three most important stepping actions you need to understand.
Step Over (F10)
Step Into (F11)
Step Out (Shift+F11)
The debugger Statement
JavaScript also provides a built-in keyword — debugger; — that you can write directly in your code. When the JavaScript engine encounters this keyword and DevTools is open, it pauses execution just like a breakpoint. This is useful when you want to quickly add a breakpoint without navigating through the DevTools interface. Just remember to remove debugger statements from your code before you ship it, since they will cause the browser to freeze if DevTools is open.
Console Logging Techniques & the Scope Panel
While console.log() is the most common console method, JavaScript provides several other useful variants. Knowing which one to use in different situations makes your debugging output much easier to read.
| Method | Purpose | Example |
|---|---|---|
console.log() | General-purpose output for any value | console.log("score:", score) |
console.warn() | Prints a yellow warning message | console.warn("score is negative!") |
console.error() | Prints a red error message | console.error("Failed to load data") |
console.table() | Displays arrays/objects as a table | console.table(students) |
console.log({name, age}) | Logs variable names with values using shorthand | // Output: {name: 'Ada', age: 16} |
console.log("score after bonus:", score). Without labels, a console full of numbers like 10, 20, 15 is almost useless because you won't remember which log printed which value.grade is still undefined because line 3 hasn't executed yet.The Scope panel is one of the most powerful parts of the debugger. Instead of manually adding console.log() for every variable, you can see all variables at once — including ones you forgot to log. You can also hover over any variable name directly in the code panel to see its value in a tooltip. This is especially useful when dealing with objects and arrays, which can be expanded to see their contents.
Worked Example — Finding a Bug with Debugging Tools
Let's walk through a realistic debugging scenario. A student has written a function to calculate the average of an array of test scores, but the function returns NaN (Not a Number) instead of the expected average. Here is the buggy code:
function average(scores) {
let sum;
for (let i = 0; i <= scores.length; i++) {
sum = sum + scores[i];
}
return sum / scores.length;
}
console.log(average([80, 90, 100]));
Expected output: 90. Actual output: NaNconsole.log("sum before loop:", sum) before the loop, and console.log("i:", i, "scores[i]:", scores[i], "sum:", sum) inside the loop.
sum before loop: undefined
i: 0 scores[i]: 80 sum: NaN
i: 1 scores[i]: 90 sum: NaN
i: 2 scores[i]: 100 sum: NaN
i: 3 scores[i]: undefined sum: NaNundefined (not 0), and the loop runs one extra time (i = 3 when array length is 3).let sum; declares sum but doesn't assign a value, so it defaults to undefined. When you add a number to undefined, JavaScript returns NaN. The fix is to write let sum = 0;i <= scores.length should be i < scores.length. When i equals 3, scores[3] is undefined because the valid indices are 0, 1, and 2. This is called an off-by-one error, one of the most common bugs in programming.
function average(scores) {
let sum = 0;
for (let i = 0; i < scores.length; i++) {
sum = sum + scores[i];
}
return sum / scores.length;
}
Running average([80, 90, 100]) now returns 90, as expected. You can remove the console.log statements since the bug is fixed.Console Logging vs. Debugger — When to Use Which
Both console logging and the debugger are essential tools, but they each have strengths and weaknesses. Experienced developers use both techniques together depending on the situation. The key is knowing which tool fits the problem you're facing.
| Feature | console.log() | Browser Debugger |
|---|---|---|
| Speed to set up | Very fast — just type a line of code | Takes a few clicks to open DevTools and set breakpoints |
| Amount of info | Only shows what you explicitly log | Shows ALL variables in scope automatically |
| Execution control | None — program runs to completion | Full control — pause, step, resume |
| Best for | Quick checks, logging over time, tracking event order | Complex logic, examining objects, understanding flow |
| Code changes needed | Yes — must add and later remove log statements | No — breakpoints are set in the browser, not in your code |
| Risk of leaving in | Clutters console if forgotten | No risk — breakpoints disappear when DevTools closes |
Connection to Advanced Debugging Concepts
The console logging and breakpoint techniques you've learned in this lesson are foundational skills that scale into much more powerful tools as you advance. Professional developers build on these same concepts, using more sophisticated versions of the same ideas.
| What You Know Now | What Comes Next |
|---|---|
console.log() to print values | Structured logging frameworks that write logs to files with timestamps, severity levels, and automatic formatting |
| Setting breakpoints by clicking line numbers | Conditional breakpoints that only pause when a specific condition is true, like i === 50 |
| Stepping through code one line at a time | Watch expressions that continuously evaluate a custom expression (like arr.length > 10) as you step |
| Reading the Scope panel | Call stack inspection — viewing the chain of function calls that led to the current line, essential for understanding complex programs |
| Debugging in the browser | IDE debuggers (like VS Code) that provide the same breakpoint and stepping features for Node.js, Python, Java, and other languages |
The mental model is always the same: inspect state, form a hypothesis, test it. Whether you're using a simple console.log() in a browser or a full-featured debugger in a professional IDE, the thought process stays identical. Mastering that thought process now will serve you in every programming language and environment you encounter in the future.
Practice Problems
console.log() and debugging with breakpoints? Explain in terms of when you see the information and whether the program pauses.let x = 5;
let y = x * 3;
console.log("y is:", y);
y = y + 2;
console.log("y is:", y);
What will be printed in the console? Write the exact two lines of output.let name = "Alice"; // line 1
let greeting = "Hello " + name; // line 2
let upper = greeting.toUpperCase(); // line 3
console.log(upper); // line 4
Remember: the breakpoint pauses before executing the marked line.function doubleAll(arr) {
let result = [];
for (let i = 0; i < arr.length; i++) {
result.push(arr[i] + arr[i]);
}
return result;
}
console.log(doubleAll(["3", "5", "7"]));
// Expected: [6, 10, 14]
// Actual: ["33", "55", "77"]
Describe how you would use console.log() to diagnose this bug. What specific log statement would reveal the problem, and what would you see in the output?console.log() everywhere and that's enough." Another classmate says: "console.log is a waste of time — just use breakpoints for everything." Who is right? Write a paragraph arguing that both are partially right but that the best approach is to use both tools. Include at least one specific scenario where console.log is the better choice and one where the debugger is the better choice.Lesson Summary
Debugging is the process of finding and fixing errors by inspecting program state — the values of variables and the position of execution at any given moment. The two essential tools for JavaScript debugging are console.log(), which prints values to the browser console without pausing your program, and the browser debugger, which lets you set breakpoints to pause execution and use stepping controls (Step Over, Step Into, Step Out) to walk through code one line at a time.
Effective debugging follows a scientific method: isolate the suspicious area, hypothesize what's wrong, then use logging or breakpoints to test your hypothesis. Remember to always label your console.log() messages so you can tell which output came from which log statement. Use console.log for quick checks and tracking values over time, and use the debugger when you need to examine complex state or understand unfamiliar code flow. Both tools are complementary, and knowing when to reach for each one is the mark of an effective programmer.