Skip to main content

How to Debug JavaScript in the Browser: DevTools, Breakpoints, and Console Methods

Every developer writes bugs. What separates experienced developers from beginners is not the number of bugs they write, but how quickly they find and fix them. Console logging is a start, but it is the slowest and least effective debugging tool available. Modern browser DevTools provide a complete debugging environment where you can pause code execution, inspect every variable, step through code line by line, and watch the call stack in real time.

This guide teaches you professional debugging techniques using Chrome DevTools (which also apply to Edge, and are similar in Firefox and Safari). You will learn how to use breakpoints effectively, inspect scope and variables, read call stacks, use powerful console methods beyond console.log, and systematically diagnose the four most common JavaScript error types.

The Sources Panel: Breakpoints and Step-Through Debuggingโ€‹

The Sources panel is the heart of JavaScript debugging in the browser. It lets you pause your code at any point and examine exactly what is happening.

Opening the Sources Panelโ€‹

Open DevTools with F12 or Ctrl + Shift + I (Windows/Linux) or Cmd + Option + I (macOS), then click the Sources tab.

The panel has three main sections:

  • File Navigator (left): Shows all loaded JavaScript files, organized by domain
  • Code Editor (center): Displays the selected file's source code
  • Debugger Sidebar (right): Shows breakpoints, scope, call stack, and watch expressions

Setting a Breakpointโ€‹

A breakpoint tells the browser to pause execution at a specific line of code. When execution reaches that line, everything stops and you can inspect the state of your program.

To set a breakpoint: Click the line number in the Sources panel. A blue marker appears on that line.

function calculateDiscount(price, discount) {
let discountAmount = price * discount; // โ† Click line number to set breakpoint
let finalPrice = price - discountAmount;
return finalPrice;
}

let result = calculateDiscount(100, 0.2);
console.log(result); // 80

When the code reaches the breakpoint line, execution pauses before that line runs. You can then:

  • Hover over any variable to see its current value
  • Examine the Scope panel to see all accessible variables
  • Step through code one line at a time

Step-Through Controlsโ€‹

When paused at a breakpoint, a toolbar appears with stepping controls:

ButtonShortcutActionDescription
โ–ถ ResumeF8ResumeContinue execution until the next breakpoint
โค“ Step OverF10Step OverExecute the current line and move to the next one. If the line contains a function call, execute the entire function without stepping into it
โค‹ Step IntoF11Step IntoIf the current line calls a function, enter that function and pause at its first line
โคŠ Step OutShift + F11Step OutExecute the rest of the current function and pause at the line after the function was called
โคป StepF9StepMove to the next executed statement (follows actual execution order)

Step-Through Exampleโ€‹

function getFullName(user) {
let first = user.firstName; // Line 2
let last = user.lastName; // Line 3
return `${first} ${last}`; // Line 4
}

function greetUser(user) {
let name = getFullName(user); // Line 8 โ† Breakpoint here
let greeting = `Hello, ${name}!`; // Line 9
return greeting; // Line 10
}

let message = greetUser({ firstName: "Alice", lastName: "Smith" });
console.log(message); // Hello, Alice Smith!

With a breakpoint on line 8:

  1. Pause at line 8 (before it executes)
  2. Step Over (F10): Executes getFullName(user) completely, moves to line 9. name is now "Alice Smith"
  3. Step Over (F10): Executes line 9, moves to line 10. greeting is now "Hello, Alice Smith!"

If instead you Step Into (F11) at line 8:

  1. Pause at line 8
  2. Step Into (F11): Enters getFullName, pauses at line 2
  3. Step Over (F10): Executes line 2, first is "Alice", moves to line 3
  4. Step Over (F10): Executes line 3, last is "Smith", moves to line 4
  5. Step Out (Shift + F11): Executes the rest of getFullName and returns to line 8, then advances to line 9

Deactivating and Removing Breakpointsโ€‹

  • Remove a breakpoint: Click the blue marker on the line number again
  • Disable without removing: Right-click the marker and select "Disable breakpoint" (or uncheck it in the Breakpoints panel)
  • Disable ALL breakpoints: Click the "Deactivate breakpoints" button in the toolbar (or press Ctrl + F8)

Types of Breakpointsโ€‹

Line breakpoints are just the beginning. DevTools offers several specialized breakpoint types for different debugging scenarios.

Line Breakpointsโ€‹

The standard breakpoint. Click a line number to pause when that line is about to execute.

Conditional Breakpointsโ€‹

A conditional breakpoint pauses only when a specified condition is true. This is invaluable in loops where you want to stop only for a specific iteration.

To set one: Right-click a line number and select "Add conditional breakpoint...", then type a JavaScript expression.

for (let i = 0; i < 1000; i++) {
processItem(items[i]);
// Right-click this line โ†’ Add conditional breakpoint โ†’ i === 500
// Pauses ONLY when i is 500
}
function processOrder(order) {
calculateTotal(order);
// Conditional breakpoint: order.total > 10000
// Only pauses for high-value orders
applyDiscount(order);
}

Logpointsโ€‹

A logpoint logs a message to the console when the line is reached, without pausing execution. It is like inserting a console.log without modifying your code.

To set one: Right-click a line number and select "Add logpoint...", then type the message using {expression} syntax for values.

function process(item) {
// Logpoint: "Processing item: {item.name}, price: {item.price}"
let result = transform(item);
return result;
}

This logs the message every time the line is reached without stopping the program. This is perfect for production debugging or when pausing would alter timing-sensitive behavior.

DOM Breakpointsโ€‹

DOM breakpoints pause when the DOM is modified in specific ways. Right-click an element in the Elements panel and select "Break on...":

  • Subtree modifications: Pauses when any child element is added, removed, or changed
  • Attribute modifications: Pauses when any attribute on the element changes
  • Node removal: Pauses when the element itself is removed

These are especially useful for debugging UI issues where you do not know which JavaScript code is modifying the DOM.

XHR/Fetch Breakpointsโ€‹

Pauses when a network request is made to a URL matching a pattern. In the Sources panel sidebar, expand "XHR/fetch Breakpoints" and click the + button.

URL contains: /api/users

This pauses whenever a fetch or XMLHttpRequest is made to any URL containing /api/users, letting you inspect the request before it is sent.

Event Listener Breakpointsโ€‹

Pauses when specific types of events fire. In the Sources panel sidebar, expand "Event Listener Breakpoints" and check the events you want to catch:

  • Mouse: click, mousedown, mouseup, mouseover
  • Keyboard: keydown, keyup, keypress
  • Timer: setTimeout fired, setInterval fired
  • Script: Script first statement (pauses when any new script begins)
  • Load: DOMContentLoaded, load

This is useful when you do not know which function handles a specific event.

Exception Breakpointsโ€‹

Pauses when any exception is thrown. Click the Pause on exceptions button (โธ with a stop sign icon) in the Sources panel toolbar.

You can choose to pause on:

  • All exceptions: Including caught ones (inside try/catch)
  • Uncaught exceptions only: Only exceptions that are not caught
try {
riskyOperation(); // "Pause on all" would stop here
} catch (e) {
handleError(e);
}

undefinedFunction(); // "Pause on uncaught" would stop here

The Watch Panel and Scope Inspectionโ€‹

When execution is paused at a breakpoint, the debugger sidebar provides detailed inspection tools.

The Scope Panelโ€‹

The Scope panel shows all variables accessible at the current point of execution, organized by scope level:

const TAX_RATE = 0.2;

function calculateTotal(items) {
let subtotal = 0;

for (let i = 0; i < items.length; i++) {
let item = items[i];
subtotal += item.price * item.quantity; // โ† Breakpoint here
}

return subtotal * (1 + TAX_RATE);
}

When paused at the breakpoint, the Scope panel shows:

โ–ธ Local
i: 1
item: {name: "Book", price: 15, quantity: 2}
subtotal: 30
items: Array(3)
โ–ธ Closure
(none in this example)
โ–ธ Script
TAX_RATE: 0.2
โ–ธ Global
window: Window
...

The scopes are listed from most specific (Local) to most general (Global). This mirrors the scope chain that JavaScript follows when resolving variable names.

The Watch Panelโ€‹

The Watch panel lets you track specific expressions across stepping operations. Click the + button and type any JavaScript expression:

items.length
subtotal / items.length
item.price > 20
typeof subtotal

Watch expressions are re-evaluated every time execution pauses, so you can see how values change as you step through code.

Hovering Over Variablesโ€‹

While paused, you can hover over any variable in the source code to see its current value in a tooltip. For objects and arrays, you can expand them to inspect nested properties directly in the tooltip.

Evaluating Expressions in the Consoleโ€‹

While paused at a breakpoint, the Console panel has access to all variables in the current scope. You can type any expression and it will be evaluated with the current state:

// While paused, type these in the Console:
> subtotal
30
> items.map(i => i.name)
["Laptop", "Book", "Pen"]
> items[i].price * items[i].quantity
30
> subtotal > 100
false

This is incredibly powerful for testing theories about bugs without modifying code.

The Call Stack and How to Read Itโ€‹

The Call Stack panel shows the chain of function calls that led to the current point of execution. Reading call stacks is a fundamental debugging skill.

Understanding the Call Stackโ€‹

function multiply(a, b) {
return a * b; // โ† Breakpoint
}

function calculateArea(width, height) {
return multiply(width, height);
}

function displayArea(shape) {
let area = calculateArea(shape.width, shape.height);
console.log(`Area: ${area}`);
}

displayArea({ width: 5, height: 10 });

When paused at the breakpoint inside multiply, the Call Stack shows:

multiply          (script.js:2)
calculateArea (script.js:6)
displayArea (script.js:10)
(anonymous) (script.js:14)

Read it bottom to top: The global script called displayArea, which called calculateArea, which called multiply, where we are now paused.

Click any function name in the Call Stack to jump to that point in the code and see the variables that were in scope when that function was called. This lets you trace back through the entire chain of execution to find where things went wrong.

Call Stack with Errorsโ€‹

When an error occurs, the browser generates a stack trace. This is the same information as the Call Stack panel but printed as text:

function getUser(id) {
return users.find(u => u.id === id); // 'users' is undefined
}

function displayProfile(userId) {
let user = getUser(userId);
console.log(user.name);
}

function handleClick() {
displayProfile(42);
}

handleClick();

The error message in the console:

Uncaught ReferenceError: users is not defined
at getUser (script.js:2:10)
at displayProfile (script.js:6:14)
at handleClick (script.js:10:3)
at script.js:13:1

Read bottom to top: line 13 called handleClick, which called displayProfile, which called getUser, where the error occurred on line 2. The error is users is not defined, meaning the variable users does not exist in the scope where getUser runs.

Async Call Stacksโ€‹

Modern DevTools show async call stacks, preserving the chain across asynchronous operations:

async function fetchUser(id) {
let response = await fetch(`/api/users/${id}`); // Breakpoint
return response.json();
}

async function loadProfile() {
let user = await fetchUser(42);
displayUser(user);
}

loadProfile();

The Call Stack shows both the async frames and the original caller:

fetchUser              (script.js:2)
async function (async)
loadProfile (script.js:7)
async function (async)
(anonymous) (script.js:11)

Console Methods Beyond logโ€‹

console.log is just one of many console methods. Using the right method for the situation makes debugging faster and console output clearer.

console.warn() and console.error()โ€‹

These produce visually distinct output in the console:

console.log("Regular message");      // Normal text
console.warn("Warning message"); // Yellow background, โš  icon
console.error("Error message"); // Red background, โœ• icon

console.error() also includes a stack trace, showing where the error was logged. Use warn for potential issues and error for actual problems:

function setAge(age) {
if (typeof age !== "number") {
console.error("setAge: age must be a number, received:", typeof age);
return;
}
if (age < 0 || age > 150) {
console.warn("setAge: unusual age value:", age);
}
this.age = age;
}

console.table()โ€‹

Displays arrays and objects as formatted tables, which is dramatically easier to read than expandable tree structures:

const users = [
{ name: "Alice", age: 28, role: "admin" },
{ name: "Bob", age: 35, role: "editor" },
{ name: "Charlie", age: 22, role: "viewer" },
];

console.table(users);

Output:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ (index) โ”‚ name โ”‚ age โ”‚ role โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 0 โ”‚ "Alice" โ”‚ 28 โ”‚ "admin" โ”‚
โ”‚ 1 โ”‚ "Bob" โ”‚ 35 โ”‚ "editor" โ”‚
โ”‚ 2 โ”‚ "Charlie" โ”‚ 22 โ”‚ "viewer" โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

You can specify which columns to display:

console.table(users, ["name", "role"]);
// Only shows 'name' and 'role' columns

console.time() and console.timeEnd()โ€‹

Measures the time between two points in your code:

console.time("array sort");

let numbers = Array.from({ length: 100000 }, () => Math.random());
numbers.sort((a, b) => a - b);

console.timeEnd("array sort");
// array sort: 23.456ms

The string label must match between time() and timeEnd(). You can run multiple timers simultaneously with different labels:

console.time("total");
console.time("fetch");

let data = await fetch("/api/data");

console.timeEnd("fetch"); // fetch: 145.2ms

console.time("process");

let result = processData(data);

console.timeEnd("process"); // process: 8.7ms
console.timeEnd("total"); // total: 154.1ms

console.group() and console.groupEnd()โ€‹

Groups related log messages together with collapsible indentation:

console.group("User Authentication");
console.log("Checking credentials...");
console.log("Username: alice@example.com");
console.log("Authentication successful");
console.groupEnd();

console.group("Loading Profile");
console.log("Fetching user data...");
console.log("Fetching preferences...");
console.warn("Profile photo not found - using default");
console.groupEnd();

Output (with collapsible groups):

โ–ผ User Authentication
Checking credentials...
Username: alice@example.com
Authentication successful
โ–ผ Loading Profile
Fetching user data...
Fetching preferences...
โš  Profile photo not found - using default

Use console.groupCollapsed() instead of console.group() to start with the group collapsed.

console.trace()โ€‹

Prints a stack trace showing how the current point of execution was reached:

function innerFunction() {
console.trace("How did we get here?");
}

function middleFunction() {
innerFunction();
}

function outerFunction() {
middleFunction();
}

outerFunction();

Output:

How did we get here?
at innerFunction (script.js:2)
at middleFunction (script.js:6)
at outerFunction (script.js:10)
at script.js:13

This is useful for understanding the execution path without setting breakpoints.

console.assert()โ€‹

Logs a message only if a condition is false. If the condition is true, nothing happens:

function processAge(age) {
console.assert(typeof age === "number", "Age should be a number, got:", typeof age);
console.assert(age >= 0, "Age should not be negative:", age);
console.assert(age <= 150, "Age seems unrealistic:", age);

// Process age...
}

processAge(25); // No output (all assertions pass)
processAge("25"); // "Assertion failed: Age should be a number, got: string"
processAge(-5); // "Assertion failed: Age should not be negative: -5"

console.count() and console.countReset()โ€‹

Counts how many times a label has been logged:

function handleEvent(type) {
console.count(type);
}

handleEvent("click"); // click: 1
handleEvent("click"); // click: 2
handleEvent("scroll"); // scroll: 1
handleEvent("click"); // click: 3
handleEvent("scroll"); // scroll: 2

console.countReset("click");
handleEvent("click"); // click: 1 (reset)

console.dir()โ€‹

Displays an interactive list of an object's properties. Especially useful for DOM elements, where console.log shows the HTML representation:

let element = document.getElementById("app");

console.log(element); // <div id="app">...</div> (HTML view)
console.dir(element); // Object view with all properties, methods, prototype

Styling Console Outputโ€‹

You can style console messages with CSS using %c:

console.log(
"%cWelcome to MyApp%c v2.0",
"color: blue; font-size: 20px; font-weight: bold;",
"color: gray; font-size: 12px;"
);

console.log(
"%c SUCCESS ",
"background: #4CAF50; color: white; padding: 2px 6px; border-radius: 3px;",
"Operation completed"
);

console.log(
"%c WARNING ",
"background: #FF9800; color: white; padding: 2px 6px; border-radius: 3px;",
"Cache is almost full"
);

Quick Reference Tableโ€‹

MethodUse Case
console.log()General output
console.warn()Potential issues (yellow)
console.error()Errors with stack trace (red)
console.table()Arrays/objects as tables
console.time()/timeEnd()Measuring performance
console.group()/groupEnd()Grouping related messages
console.trace()Stack trace at any point
console.assert()Log only when condition fails
console.count()Count executions
console.dir()Object property inspection
console.clear()Clear the console

The debugger Statementโ€‹

The debugger statement is a breakpoint written directly in your code. When DevTools is open and the JavaScript engine encounters debugger, it pauses execution exactly like a breakpoint set in the Sources panel.

Basic Usageโ€‹

function processData(data) {
let result = transform(data);

debugger; // Execution pauses here when DevTools is open

return validate(result);
}

When to Use debuggerโ€‹

The debugger statement is useful when:

  • You want to share a breakpoint with teammates (it lives in the code)
  • You are debugging dynamically loaded or generated code where setting breakpoints in Sources is difficult
  • You want a breakpoint that is conditional on complex logic
// Conditional debugging
function processItem(item, index) {
if (item.type === "special" && index > 100) {
debugger; // Only pause for special items after index 100
}
// process...
}
warning

Always remove debugger statements before committing code. If DevTools is not open, debugger is silently ignored, but it should never appear in production code. Configure ESLint to catch it:

{
"rules": {
"no-debugger": "error"
}
}

debugger vs. Breakpointsโ€‹

Featuredebugger statementDevTools Breakpoint
Set in codeYesNo (set in DevTools UI)
Shared with teamYes (via version control)No (local to your browser)
Survives page reloadYes (it is in the code)Yes (DevTools remembers)
Risk of going to productionYesNo
ConditionalVia surrounding ifBuilt-in condition field
Requires DevTools openFor pausing, yesYes

Debugging Common Errorsโ€‹

JavaScript has four main error types that you will encounter regularly. Recognizing them instantly and knowing where to look saves enormous debugging time.

ReferenceError: Variable Not Foundโ€‹

A ReferenceError occurs when code references a variable that does not exist in the current scope.

function greet() {
console.log(mesage); // ReferenceError: mesage is not defined
}
greet();

Common causes:

// 1. Typo in variable name
let message = "Hello";
console.log(mesage); // 'mesage' (missing 's')

// 2. Variable used outside its scope
if (true) {
let secret = 42;
}
console.log(secret); // ReferenceError (block-scoped)

// 3. Using before declaration (TDZ)
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 10;

// 4. Forgot to import or include a library
moment().format(); // ReferenceError: moment is not defined

Debugging strategy: Check spelling, check scope, check declaration order. The error message tells you exactly which variable name is the problem.

TypeError: Wrong Type for an Operationโ€‹

A TypeError occurs when a value is not the type expected for an operation.

// 1. Calling a non-function
let x = 42;
x(); // TypeError: x is not a function

// 2. Accessing property of null/undefined
let user = null;
console.log(user.name); // TypeError: Cannot read properties of null

// 3. Calling a method on wrong type
let num = 42;
num.toUpperCase(); // TypeError: num.toUpperCase is not a function

// 4. Assigning to read-only property (strict mode)
const obj = Object.freeze({ name: "Alice" });
obj.name = "Bob"; // TypeError: Cannot assign to read only property

Debugging strategy:

// The error "Cannot read properties of null/undefined" is the most common TypeError
// Always check the value BEFORE the dot:

// Error: Cannot read properties of undefined (reading 'name')
// means: something.name where 'something' is undefined

// Find WHAT is undefined:
function getUsername(response) {
// If this crashes with "Cannot read properties of undefined (reading 'name')"
return response.data.user.name;

// Debug by checking each part:
console.log("response:", response); // Is this defined?
console.log("response.data:", response.data); // Is this defined?
console.log("response.data.user:", response.data.user); // Is this defined?

// Or use optional chaining:
return response?.data?.user?.name;
}

SyntaxError: Invalid Code Structureโ€‹

A SyntaxError means the JavaScript engine cannot parse your code. The code never runs because it fails during parsing.

// 1. Missing/extra brackets
function greet(name { // SyntaxError: Unexpected token '{'
console.log(name);
}
// 2. Missing comma in object/array
let user = {
name: "Alice"
age: 30 // SyntaxError: Unexpected identifier 'age' (missing comma)
};
// 3. Invalid keyword usage
let class = "Math"; // SyntaxError: Unexpected token 'class'
// 4. Unterminated string
let message = "Hello; // SyntaxError: Invalid or unexpected token
// 5. Invalid JSON
JSON.parse("{ name: 'Alice' }"); // SyntaxError: Expected property name or '}'
// JSON requires double quotes: '{ "name": "Alice" }'

Debugging strategy: The error message includes a line number. Look at that line and the line before it (often the actual problem is on the preceding line). Check for missing brackets, commas, quotes, and reserved words used as identifiers.

RangeError: Value Out of Acceptable Rangeโ€‹

A RangeError occurs when a numeric value is outside the allowed range.

// 1. Infinite recursion (call stack overflow)
function recursive() {
recursive(); // RangeError: Maximum call stack size exceeded
}
recursive();
// 2. Invalid array length
let arr = new Array(-1); // RangeError: Invalid array length
// 3. Invalid number methods
let num = 1.5;
num.toFixed(200); // RangeError: toFixed() digits argument must be between 0 and 100
// 4. Invalid radix
(42).toString(37); // RangeError: toString() radix must be between 2 and 36

Debugging strategy: For stack overflow errors, look for recursive functions that lack a proper base case. The call stack in DevTools will show the repeating function call.

Quick Error Diagnosis Tableโ€‹

ErrorLikely CauseFirst Thing to Check
ReferenceError: X is not definedTypo, scope issue, missing importSpelling, variable scope
TypeError: Cannot read properties of null/undefinedAccessing property on missing valueWhat is null/undefined and why?
TypeError: X is not a functionCalling a non-function, wrong variableValue and type of the variable
SyntaxErrorMalformed codeThe line mentioned and the one before it
RangeError: Maximum call stackInfinite recursionBase case in recursive function

A Systematic Debugging Processโ€‹

When you encounter any error, follow this process:

1. READ the error message carefully
- What type of error?
- What variable/property is mentioned?
- What line number?

2. LOOK at the stack trace
- What function caused the error?
- What called that function?

3. REPRODUCE the error
- What inputs cause it?
- Does it happen every time?

4. SET a breakpoint just before the error
- Inspect variable values
- Check if values are what you expect

5. FIX and VERIFY
- Make the smallest possible change
- Test with the same inputs that caused the error
- Test with other inputs to make sure nothing else broke

Summaryโ€‹

Professional JavaScript debugging uses a combination of DevTools features and systematic techniques:

  • The Sources panel lets you set breakpoints and step through code line by line. Use Step Over to skip function internals, Step Into to explore them, and Step Out to return to the caller.
  • Beyond line breakpoints, use conditional breakpoints for loops, logpoints for non-pausing logging, DOM breakpoints for tracking element changes, XHR breakpoints for network requests, and event listener breakpoints for tracing user interactions.
  • The Watch panel tracks custom expressions, the Scope panel shows all accessible variables organized by scope level, and hovering over variables while paused shows their current values.
  • Read the Call Stack bottom-to-top to understand the chain of function calls leading to the current point. Click entries to navigate to different points in the call chain.
  • Use the right console method for the job: console.table() for data, console.time() for performance, console.group() for organization, console.trace() for call paths, and console.assert() for conditional logging.
  • The debugger statement places breakpoints directly in code. Always remove them before committing.
  • Recognize the four common error types instantly: ReferenceError (variable not found), TypeError (wrong type for operation), SyntaxError (malformed code), and RangeError (value out of bounds).
  • Follow a systematic debugging process: read the error, check the stack trace, reproduce the bug, set a breakpoint, inspect state, fix, and verify.

With these debugging skills, you will find and fix bugs in minutes instead of hours. The next step in code quality is establishing consistent coding style and conventions, which prevents many bugs from occurring in the first place.