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 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:
The code cannot be parsed. The file will not execute at all until the syntax is fixed. Fix this before debugging anything else.
A variable was referenced that does not exist in the current scope. The code executed up to this point, but this variable is undefined.
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.
A value is outside the acceptable range for an operation. The most common form is "Maximum call stack size exceeded" from infinite recursion.
The fetch or XHR request could not be completed. These errors occur outside the JavaScript runtime and require the Network tab to diagnose properly.
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.
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.
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:
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.
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.
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:
| 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 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 |
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
- Open DevTools (F12) and click the Sources tab.
- Navigate to the JavaScript file in the file tree on the left, or press Cmd+P (Chrome) to search for the file by name.
- Click the line number where you want execution to pause. A blue marker appears.
- Trigger the code path that runs that line (click a button, submit a form, reload the page).
- 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.
- 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.
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.
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
- Open DevTools (F12) and click the Network tab. Make sure recording is active (the red dot in the top-left corner should be filled).
- Trigger the failing action: click the button, submit the form, or reload the page that makes the API call.
- Find the request in the list. Filter by "XHR" or "Fetch" to hide image and CSS requests. Failed requests appear in red.
- Click the request to open the detail panel. Check the Status code: 200, 404, 401, 500?
- Click the Response tab to see exactly what the server returned. This is the actual data your JavaScript code is receiving.
- Click the Headers tab to inspect request headers. Are you sending the Authorization header? Is the Content-Type set correctly?
- Click the Payload tab (or Request body) to see exactly what your code sent to the server. Does it match what the API expects?
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:
- 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.
- 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."
- 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?
- 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?
- 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.
- 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.
- 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:
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.
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.
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.
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.
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.
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.
Frequently Asked Questions About Debugging JavaScript
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.
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.
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.
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.
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.
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.
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.