Menu

JavaScript Debugging DevTools Developer Skills March 2026 ⏱ 15 min read

How to Debug JavaScript Errors : A Complete Developer Guide

A systematic debugging process that works for every JavaScript error in every environment. Error types explained, DevTools walkthrough, breakpoints, Network tab, and fixes for the most common bugs.

Debugging is where junior and senior developers diverge most visibly. Junior developers guess. Senior developers have a systematic process. This guide gives you that process. Learn how to read JavaScript error messages properly, use browser DevTools effectively, set breakpoints, diagnose API failures, and fix the most common JavaScript errors with confidence.

The Debugging Mindset

Before touching any tool, adopt one principle: debugging is not guessing. It is elimination. You start with a failing system, form a hypothesis about the cause, test the hypothesis, and either fix it or form a new hypothesis based on what you learned. Each test eliminates possibilities until only one cause remains.

🎲
Guessing
Random changes without understanding
Change a variable. Refresh. Try something else. Add a console.log somewhere random. Delete the thing you added. Comment out half the function. Repeat for 2 hours.
🔬
Systematic Debugging
Hypothesis, test, eliminate, repeat
Read the error completely. Identify the exact failing line. Check what every variable contains at that moment. Form a specific hypothesis. Test it. Confirm or rule it out and move to the next.

Guessing sometimes works. Systematic debugging always does. The difference between a 20-minute fix and a 4-hour fix is usually whether the developer read the error message completely.

The single most common debugging mistake is not reading the error message carefully. JavaScript error messages tell you the error type, the exact message, the file name, and the line number. That is usually enough information to identify the problem before opening a single tool. Treat every error message as the starting point of a directed search, not as noise to scroll past on the way to adding console.log statements.

JavaScript Error Types: Know What You Are Dealing With

JavaScript has a defined set of built-in error types. Recognising the error type immediately tells you what class of problem you are dealing with and narrows the list of possible causes before you look at a single line of code:

SyntaxError Parsing failed

The code cannot be parsed. The file will not execute at all until the syntax is fixed. Fix this before debugging anything else.

Common causes
Missing closing bracket or brace, extra comma, missing quotes, invalid token in an unexpected position.
ReferenceError Variable not found

A variable was referenced that does not exist in the current scope. The code executed up to this point, but this variable is undefined.

Common causes
Variable never declared, out of scope (declared inside a block or function), typo in variable name, used before declaration with let/const.
TypeError Wrong type

The most common JavaScript error. An operation was performed on a value of the wrong type. Almost always means something you expected to exist is undefined or null.

Common causes
Accessing a property on undefined, calling a non-function as a function, calling .map() on null instead of an array.
RangeError Value out of range

A value is outside the acceptable range for an operation. The most common form is "Maximum call stack size exceeded" from infinite recursion.

Common causes
Function calling itself without a base case, passing a negative length to Array(), number too large or too small for the operation.
NetworkError HTTP request failed

The fetch or XHR request could not be completed. These errors occur outside the JavaScript runtime and require the Network tab to diagnose properly.

Common causes
Server not running, wrong URL, CORS headers missing, network connectivity issue, wrong port.

SyntaxError: The File Won't Even Run

A SyntaxError means JavaScript could not parse your code at all. The entire file fails to execute, not just the line with the error. This makes it the most urgent error to fix and usually the easiest: the browser tells you exactly where the problem is.

SyntaxError: Unexpected token '}' at script.js:24:1
JavaScript
// Common SyntaxError causes // Missing closing parenthesis if (user.isLoggedIn { // SyntaxError: missing ) doSomething(); } // Trailing comma in older environments const obj = { name: 'Alice', email: '[email protected]', // Trailing comma (fine in modern JS) } // Missing closing bracket in JSX return ( <div> <p>Hello</p> // Missing </div> — SyntaxError in JSX );
✅ Fix SyntaxErrors with a linter, not by guessing

Install ESLint in your editor. A properly configured ESLint setup highlights SyntaxErrors in real time as you type, before you even save the file. Spending 10 minutes configuring ESLint once eliminates an entire class of debugging sessions. Prettier also catches many syntax issues through automatic formatting. If your code will not format cleanly, it is likely a syntax error preventing the formatter from parsing it.

ReferenceError: Variable Not in Scope

A ReferenceError means JavaScript could not find a variable you tried to use. The code ran up to that point successfully, which means the variable either was never declared, exists in a different scope, or has a typo in its name.

ReferenceError: userData is not defined at processUser (app.js:42:14) at handleSubmit (app.js:18:3)
JavaScript
// Cause 1: Variable declared in a different scope function fetchUser() { const userData = { name: 'Alice' }; // Only exists inside fetchUser } function processUser() { console.log(userData.name); // ReferenceError: userData is not defined } // Cause 2: Temporal dead zone (let/const used before declaration) console.log(count); // ReferenceError: cannot access before initialization let count = 0; // Cause 3: Typo in variable name const userProfile = { name: 'Alice' }; console.log(userprofile.name); // ReferenceError: userprofile is not defined // (capital P missing)
⚠ Read the stack trace, not just the error line

The stack trace below the error message shows the full call chain that led to the error. Reading it bottom-to-top shows how execution arrived at the failing line. The bottom of the stack is where your code started, the top is where it crashed. This is how you find the originating problem in complex call chains where the error appears far from where the bad value was created.

TypeError: The Most Common JavaScript Error

TypeErrors are the error you will encounter most frequently in JavaScript development. They occur when you try to perform an operation on a value of the wrong type. The three most common forms each have a specific diagnosis:

TypeError: Cannot read properties of undefined (reading 'email') at renderUser (components/UserCard.js:12:24) TypeError: response.json is not a function at fetchUserData (api/users.js:8:22) TypeError: items.map is not a function at ProductList (components/ProductList.jsx:31:14)

Each of these error messages is telling you something specific. "Cannot read properties of undefined (reading 'email')" means you have a variable that you expect to be an object, but it is undefined. "is not a function" means you have a variable that you expect to be a function, but it is something else. "items.map is not a function" means items is not an array: it is null, undefined, or an object.

The diagnostic step for every TypeError is the same: add a console.log immediately before the failing line and log every variable the line depends on. Check both the value and the type (console.log(typeof items, items)). The value will almost always be undefined, null, or something unexpected.

JavaScript — diagnosing a TypeError
// The failing line const email = user.email; // TypeError: Cannot read properties of undefined // Step 1: Log the value immediately before the failing line console.log('user value:', user); // What is it? console.log('user type:', typeof user); // What type is it? const email = user.email; // Once you confirm user is undefined, trace backward: // Where was user set? Follow the value back through the code. // Step 2: Fix — use optional chaining for values that may be absent const email = user?.email; // Returns undefined instead of throwing const email = user?.email ?? ''; // Returns '' as fallback

RangeError: Value Out of Bounds

RangeErrors occur when a value is outside the acceptable range for an operation. The most common in practice is "Maximum call stack size exceeded," which almost always means infinite recursion: a function calling itself without a termination condition that stops the recursion.

RangeError: Maximum call stack size exceeded at flatten (utils.js:14:12) at flatten (utils.js:14:12) at flatten (utils.js:14:12) ... (repeated 10,000+ times)
JavaScript
// Infinite recursion — no base case function countdown(n) { console.log(n); return countdown(n - 1); // Never stops — RangeError } // Fixed — base case stops the recursion function countdown(n) { if (n <= 0) return; // Base case console.log(n); return countdown(n - 1); } // Other RangeError examples new Array(-1); // RangeError: Invalid array length (1.5).toFixed(200); // RangeError: toFixed() digits argument must be 0-100

The telltale sign of a RangeError from infinite recursion is a stack trace where the same function name repeats hundreds or thousands of times. The fix is always to identify the missing base case: the condition that should stop the recursion but does not.

Network Errors: When the Request Never Arrives

Network errors occur when a fetch or XHR request fails at the HTTP layer before the server even has a chance to respond. These require the Network tab in DevTools to diagnose, not the Console tab. Three distinct problems produce similar-looking "fetch failed" messages in the console:

TypeError: Failed to fetch at fetchUserData (api/users.js:6:18) Error: net::ERR_CONNECTION_REFUSED Error: Access to fetch at 'https://api.example.com' from origin 'http://localhost:3000' has been blocked by CORS policy
Error Message Meaning First Check
Failed to fetch Request could not be made at all. Generic catch-all for network failures. Is the server running? Is the URL correct including port number?
ERR_CONNECTION_REFUSED Server is not listening on that address and port. Nothing running there. Start the backend server. Check the port matches your fetch URL.
CORS policy blocked Browser blocked the request because the server did not include CORS headers allowing your origin. Add Access-Control-Allow-Origin header to the server response for your frontend's origin.
ERR_NAME_NOT_RESOLVED The hostname in the URL does not resolve to an IP address. DNS failure or typo in the domain. Check the URL for typos. Confirm the domain exists and your DNS is working.
ERR_CERT_INVALID SSL/TLS certificate on the server is invalid, expired, or self-signed without trust. Renew the SSL certificate. In development, trust the self-signed certificate in your browser settings.

Browser DevTools: Your Primary Debugging Environment

Browser DevTools are the most powerful debugging environment available for JavaScript. Open them with F12 on Windows or Linux, and Cmd+Option+I on macOS. Every major browser (Chrome, Firefox, Safari, Edge) includes a full suite of debugging tools. Here are the five tabs you will use most:

Console
Error messages and logging
Every JavaScript error appears here with its type, message, file, and line number. Also where console.log output appears.
F12 then Console tab
Sources
Breakpoints and step debugging
Set breakpoints to pause execution and inspect all variable values at that exact moment in time.
F12 then Sources tab
Network
HTTP request inspection
Every request your page makes, with full request and response details. Essential for API debugging.
F12 then Network tab
Elements
DOM and CSS inspection
Inspect the live DOM structure and applied CSS. Edit both in real time to test UI changes without modifying source files.
F12 then Elements tab
Application
Storage and service workers
Inspect localStorage, sessionStorage, cookies, IndexedDB, and service worker state. Useful for debugging auth and caching bugs.
F12 then Application tab

Console Tab: More Than Just console.log

The Console tab is your first stop for any JavaScript error. Every error appears here automatically with a clickable link to the exact file and line. Beyond reading errors, the console offers a full toolkit of methods for different debugging situations:

Method Use Case When to Reach for It
console.log() Output any value to the console General purpose: checking variable values, confirming code execution reached a point
console.error() Output styled as a red error Logging caught errors in catch blocks so they stand out from regular log output
console.warn() Output styled as a yellow warning Flagging conditions that are not errors but should be investigated
console.table() Display arrays of objects as a formatted table Inspecting API responses, lists of records, or any structured array data: vastly more readable than a collapsed object
console.group() Group related log output under a collapsible label Organising debug output for repeated operations like loop iterations or multiple API calls
console.time() / timeEnd() Measure elapsed time between two points Profiling how long an operation takes without a full performance tool setup
console.trace() Print the current call stack Understanding how code arrived at a specific point when the call chain is not obvious
console.assert() Log only when a condition is false Asserting invariants during debugging without adding conditional logic to the code
JavaScript — console debugging patterns
// Most underused: console.table for inspecting arrays of objects const users = [ { id: 1, name: 'Alice', role: 'admin' }, { id: 2, name: 'Bob', role: 'user' }, ]; console.table(users); // Renders as a clean table — far more readable than collapsed object // Checking type AND value together console.log(typeof response, response); // Timing an operation console.time('api-call'); await fetchUsers(); console.timeEnd('api-call'); // api-call: 243ms // Grouping loop output items.forEach((item, i) => { console.group(`Item ${i}`); console.log('value:', item); console.log('valid:', isValid(item)); console.groupEnd(); });

Breakpoints: The Most Powerful Tool You Are Probably Not Using

A breakpoint pauses JavaScript execution at a specific line so you can inspect the state of every variable at that exact moment. This is significantly more powerful than adding console.log statements throughout the code: you see everything all at once, you can step forward line by line, and you do not have to remove debug statements afterward.

Setting a Standard Breakpoint

  1. Open DevTools (F12) and click the Sources tab.
  2. Navigate to the JavaScript file in the file tree on the left, or press Cmd+P (Chrome) to search for the file by name.
  3. Click the line number where you want execution to pause. A blue marker appears.
  4. Trigger the code path that runs that line (click a button, submit a form, reload the page).
  5. Execution pauses at the breakpoint. Hover over any variable to see its current value, or check the Scope panel on the right for all variables in scope.
  6. Use keyboard shortcuts to navigate: F10 to step over (execute the line without going inside functions), F11 to step into (follow execution into a function call), F8 to resume execution normally.
JavaScript — you can also set breakpoints in code
// The debugger statement pauses execution in DevTools function processOrder(order) { debugger; // DevTools will pause here when this line is reached const total = calculateTotal(order.items); return { ...order, total }; }

Conditional Breakpoints

When a bug only occurs on specific data (for example, one particular user ID out of thousands), a standard breakpoint that pauses on every iteration wastes time. Conditional breakpoints pause execution only when a specific expression is true:

Right-click a line number in the Sources tab and select "Add conditional breakpoint." Enter a JavaScript expression, such as user.id === 42 or items.length === 0. The debugger only pauses when the expression evaluates to true, letting you skip thousands of normal iterations and land directly on the case that is failing.

💡 Logpoints: console.log without touching source code

Right-click a line number in the Sources tab and select "Add logpoint." Enter an expression (like user.id, user.email) and DevTools logs it to the Console every time that line executes, without pausing execution and without modifying your source code. Logpoints are ideal for debugging production builds or scenarios where pausing execution would mask the bug (like timing-dependent issues).

Network Tab: Debugging API Calls

The Network tab shows every HTTP request your page makes, with the complete request and response for each one. When an API integration is failing, the Network tab tells you exactly what was sent and what came back, which is usually the entire answer.

How to Inspect a Failed API Call

  1. Open DevTools (F12) and click the Network tab. Make sure recording is active (the red dot in the top-left corner should be filled).
  2. Trigger the failing action: click the button, submit the form, or reload the page that makes the API call.
  3. Find the request in the list. Filter by "XHR" or "Fetch" to hide image and CSS requests. Failed requests appear in red.
  4. Click the request to open the detail panel. Check the Status code: 200, 404, 401, 500?
  5. Click the Response tab to see exactly what the server returned. This is the actual data your JavaScript code is receiving.
  6. Click the Headers tab to inspect request headers. Are you sending the Authorization header? Is the Content-Type set correctly?
  7. Click the Payload tab (or Request body) to see exactly what your code sent to the server. Does it match what the API expects?
💡 Paste response bodies into the JSON Formatter before writing parsing code

The Network tab shows response JSON in a minimal format that is hard to read for complex structures. Copy the response body and paste it into the JSON Formatter to see it clearly indented with the full nesting visible. This reveals field names, data types, nesting levels, and null values that are easy to miss in the compressed Network tab view. Many "API bugs" turn out to be misread response structures that become obvious once the JSON is properly formatted.

The 7-Step Debugging Process

Follow this sequence for any JavaScript bug, in any environment, in any framework. It works because it forces you to gather information before forming conclusions, which eliminates the guessing cycle that most developers fall into:

  1. Read the error message completely. Do not skim. Read the full error: the type (TypeError, ReferenceError, etc.), the message, the file name, and the line number. Most errors tell you exactly what went wrong if you read them carefully rather than immediately jumping to the code. The error type alone narrows the possible causes significantly.
  2. Find the failing line. Click the file and line number link in the error message (in the Console tab, it is always a clickable link). Go to that exact line. What is it doing? What variables does it access? What function does it call? You now have a specific, concrete location to investigate rather than a vague sense that "something is wrong."
  3. Check every variable the failing line depends on. Add console.log statements immediately before the failing line and log every variable the line uses. For each one, log both the value and the type: console.log(typeof user, user). Is any variable undefined or null when it should not be? Is any value a different type than you expected?
  4. Trace the bad value backward. When you find a variable with an unexpected value, the real bug is usually upstream: wherever that value was set or transformed incorrectly. Follow the value backward through the code: where was it assigned? Was it returned from a function that might be returning undefined on some code path? Was it fetched from an API that returned an unexpected structure?
  5. Form a specific, testable hypothesis. Based on what you found, form a precise hypothesis: "The API returns null for the user.address field when the user has never set an address, and the code does not handle this case." A specific hypothesis is testable. A vague sense that "something is wrong with the API call" is not.
  6. Test the hypothesis directly. Do not fix the code yet. Test whether your hypothesis is correct: log the raw API response, add a breakpoint at the exact moment the value is set, or add a temporary check that logs whether the condition you identified is actually occurring. Confirm your hypothesis before modifying any code. This prevents the situation where your fix does not work because you were solving the wrong problem.
  7. Fix and verify the original scenario. Make the minimal fix that addresses the confirmed root cause. After fixing, test the exact scenario that originally triggered the bug to confirm it is resolved. Also test adjacent scenarios to confirm the fix does not introduce new failures. Never declare a bug fixed without re-running the exact conditions that caused it.

Common JavaScript Bugs and Their Fixes

These are the bugs that appear most frequently in JavaScript development. Each one has a consistent pattern: a cause, a diagnostic step, and a reliable fix:

TypeError Cannot read properties of undefined (reading 'X') Very Common

The most common JavaScript error. Something you expected to be an object is undefined. This usually happens when an API response is null or missing a field, a function returns undefined instead of an object, or data is accessed before it is loaded asynchronously.

Bug
// Assuming user is always defined const email = user.email; // TypeError if user is undefined
Fix: optional chaining
// Returns undefined instead of throwing const email = user?.email; // With a fallback value const email = user?.email ?? '';
TypeError items.map is not a function Very Common

You are calling an array method on a value that is not an array. The API returned null, an object, or undefined instead of the array your code expected. Always validate the type before calling array methods.

Bug
// API returned null, not an array items.map(item => item.name); // TypeError: items.map is not a function
Fix: guard with Array.isArray
// Ensure it is always an array const safeItems = Array.isArray(items) ? items : []; safeItems.map(item => item.name);
JavaScript — diagnosing the type first
// Always check type AND value before assuming console.log(typeof items, items); // If this logs "object null" — items is null, not an array // If this logs "object {data: [...]}" — items is wrapped in an object
TypeError response.json is not a function Common

You are calling .json() on something that is not a Fetch API Response object. This usually happens when the fetch call itself failed and returned an error object, or when you are working with a library that returns already-parsed data rather than a raw Response.

Bug
const data = await fetch(url); // Forgot await on the .json() call: const json = data.json; // Not a function call
Fix: correct fetch pattern
const response = await fetch(url); if (!response.ok) throw new Error(response.status); const data = await response.json();
Logic Bug Bug only appears in production, not in development Common

When a bug appears in production but not in development, the cause is almost always a data difference between environments: different API responses, different user data edge cases, different environment variables, or different API endpoints. The fix starts with finding exactly what is different.

Diagnostic approach
// Step 1: Log the raw API response in both environments const response = await fetch(url); const data = await response.json(); console.log('Raw API response:', JSON.stringify(data, null, 2)); // Step 2: Paste both responses into the Text Diff Checker // at stackdevtools.com/text-diff-checker/ // The difference between them is usually the entire bug.

Paste the production response and the development response side by side into the Text Diff Checker. The highlighted differences reveal the exact data discrepancy causing the bug. Common findings: a field that is null in production but always populated in development, an array that is empty in production but always has items in development, a date format that differs between environments.

Async Bug Variable is undefined even though the fetch was successful Common

Accessing data before an async operation completes is one of the most common bugs for developers learning asynchronous JavaScript. The code runs in the correct order for a human reading it top-to-bottom, but the runtime executes async operations without waiting for them unless you explicitly tell it to with await.

Bug: missing await
let user; fetch('/api/user/1') .then(res => res.json()) .then(data => { user = data; }); console.log(user.name); // undefined — fetch not done yet
Fix: await the async operation
async function loadUser() { const res = await fetch('/api/user/1'); const user = await res.json(); console.log(user.name); // Works correctly }

Frequently Asked Questions About Debugging JavaScript

What is the fastest way to find a JavaScript bug?

Read the error message completely and click the file and line number link it contains. The error message tells you the type, the specific failure, and the exact location. Most developers skim error messages and then spend 30 minutes guessing, when the answer was in the first sentence. If the error message alone does not reveal the cause, the next fastest approach is to add a console.log immediately before the failing line and log every variable that line depends on. Seeing the actual values at that moment eliminates guesswork and usually identifies the cause within one or two iterations.

When should I use breakpoints instead of console.log?

Use breakpoints when you need to inspect the full state of the application at a specific moment, not just one or two variables. Breakpoints let you see all variables in scope simultaneously, step through code line by line, and watch how values change. Use console.log when you want a quick check of a specific value, when you need to track values over multiple iterations of a loop, or when pausing execution would mask the bug (such as in timing-sensitive code). For most day-to-day debugging, a combination of both is most effective: console.log to identify which function has the problem, breakpoints to understand exactly what is happening inside it.

How do I debug JavaScript errors in production when DevTools are not available?

Use a JavaScript error monitoring service like Sentry, Bugsnag, or Rollbar. These tools capture unhandled exceptions in production with the full stack trace, the user's browser and OS, the URL where the error occurred, and a breadcrumb trail of recent actions. They also capture the error context, which is often enough to reproduce and fix the bug without direct access to the user's environment. For API-related bugs, structured logging on the backend with correlation IDs that link frontend requests to backend log entries makes tracing production bugs significantly faster. Log the raw API responses in development using console.log(JSON.stringify(data, null, 2)) and paste them into the JSON Formatter to understand the response structure before writing parsing code.

What is a CORS error and how do I fix it?

CORS (Cross-Origin Resource Sharing) is a browser security mechanism that blocks web pages from making requests to a different origin (domain, port, or protocol) than the page itself, unless the server explicitly allows it. A CORS error means the server did not include the necessary Access-Control-Allow-Origin header in its response. The fix is always on the server side, not the browser or frontend: the server needs to add the header that allows your frontend's origin. For development, add your localhost URL. For production, add your production domain. CORS errors do not occur in server-to-server requests (like Postman or curl), only in browser requests, which is why an endpoint can work in Postman but fail in the browser.

How do I debug JavaScript in a Node.js backend (not the browser)?

For Node.js, you have three main options. First, console.log and console.error work exactly as in the browser. Second, the Node.js built-in debugger: run your script with node --inspect app.js, then open Chrome and navigate to chrome://inspect to connect to it with the full Chrome DevTools interface including breakpoints, scope inspection, and step debugging. Third, VS Code has excellent built-in Node.js debugging: create a launch configuration in .vscode/launch.json, set breakpoints in your editor, and press F5 to start debugging. VS Code shows all variables in scope directly in the editor alongside the code, which many developers find more readable than switching between DevTools tabs.

What is the best way to compare API responses between development and production?

Log the raw JSON response in both environments using console.log(JSON.stringify(data, null, 2)) or by copying the response body from the Network tab in DevTools. Paste both responses into the Text Diff Checker side by side: the tool highlights every addition, deletion, and change between the two versions. This approach reliably surfaces field differences, type differences (string vs number), null fields, missing fields, and structural changes that would be easy to miss by reading both responses manually. It is also useful when an API updates its response format and you need to understand exactly what changed between the old and new version.

Free browser-based developer tools

Tools that make JavaScript debugging faster

Format and validate JSON API responses, compare response bodies between environments, convert data formats, and more. All free, all in your browser, no login required.

Systematic Debugging Is a Skill. Build It Deliberately.

Every developer who appears to "magically" fix bugs fast is applying a systematic process, not intuition. They read error messages completely. They check variable values before forming conclusions. They test hypotheses directly before modifying code. They use breakpoints to see the full application state at the exact moment of failure.

The five error types in this guide cover the overwhelming majority of JavaScript bugs you will encounter. Recognising the error type immediately narrows the cause. The 7-step process works for every bug in every environment. The DevTools features covered here: conditional breakpoints, logpoints, the Network tab, and console.table, are the tools that separate developers who debug efficiently from those who guess.

Keep the JSON Formatter open whenever you are working with API responses. Paste every response body into it before writing code that depends on its structure. For bugs that only appear in production, the Text Diff Checker finds the exact data difference between your environments in seconds. The combination of a systematic process and the right tools resolves the vast majority of JavaScript bugs before they turn into multi-hour debugging sessions.