Skip to content
JavaScript Debugging: How to Find and Fix Bugs in JS

19 Minutes

JavaScript Debugging: How to Find and Fix Bugs in JS

Fix Bugs Faster! Log Collection Made Easy

Get started

An effective JavaScript debugging regime is essential if we want to build responsive, reliable and highly-rateable Android apps.

JavaScript doesn’t enforce types at compile time (unlike Swift) and this means errors often happen quietly, when users are already feeling them. So it’s vital that we debug pre-emptively, using knowledge rather than guesswork.

Debugging is the reason Bugfender exists, and we’ve created this post to give you a full armoury of tips and techniques, so you can debug JavaScript efficiently at scale.

  • We’ll give you a step-by-step guide to debugging.
  • Then we’ll provide some deep-dives into the various tools at your disposal.
  • Finally, we’ll give you some tips to take into your day-to-day work.

Here’s the full list of contents, so you can jump straight to the information you need – or carry on reading if you want the entire knowledge bank.

💡

Quick caveat: this guide focuses on debugging runtime behavior and logic issues, not exception-handling patterns like try-catch. Want to read about that? Try this instead.

The four pillars of JavaScript debugging

Debugging works best when we follow a clear process instead of jumping randomly between logs, breakpoints and fixes. In fact most issues can be solved quickly using this four-step process:

  1. Recognize the bug signals by observing errors, wrong behavior, timing issues or production-only failures.
  2. Choose and use the corresponding debugging workflow based on that signal, before you dip into the tools.
  3. Verify the JavaScript fix by reproducing the original scenario and confirming that the behavior is now correct.
  4. Prevent the bug from coming back by adding guards, defaults, logging or basic coverage.

This approach keeps debugging predictable, efficient, and easier to repeat across projects.

Step 1: Recognize the JavaScript bug signals

To recognize the type of JavaScript bug we’re dealing with, we must initially focus on the signal, not the code.

Before we open DevTools, we can observe what the system actually tells us via these methods:

  • Look for explicit errors: check the console for TypeError, ReferenceError, SyntaxError or a stack trace pointing to a file and line.
    • Open DevTools via right-click → Inspect, or Cmd + Option + I on macOS, Ctrl + Shift + I on Windows/Linux).
  • Observe the behavior: does the code run but produce the wrong output, undefined values, or incorrect UI with no error?
  • Watch for timing clues: does the issue appear only sometimes, during loading, or depending on execution order or network speed?
  • Check the environment: does it work locally but fail in production, on certain browsers, or specific devices?

These signals are usually enough to identify the bug type and move to the correct debugging workflow, rather than just guessing.

The most common JavaScript bugs that developers run into

What breaks in JavaScriptTell-tale signs, typical causes, and workflow
Runtime errorsJavaScript throws a visible TypeError, ReferenceError or SyntaxError, usually with a stack trace.
Undefined valuesA variable or property is undefined
, no error is thrown, but the UI or result is wrong. Common causes include missing data, incorrect object paths, or code running before initialization → Behavior-based debugging
Async timing issuesFunctions run without errors but return incorrect or missing values in some code paths. Often caused by off-by-one errors, conditional logic gaps, or unexpected type coercion → Behavior-based debugging
Async timing issuesLoading never ends, works only sometimes, or its behavior changes based on order or timing. Usually caused by race conditions, unresolved promises, or incorrect async/await
usage → Async and network debugging
Network-related bugsRequests fail silently, return unexpected data, or arrive too late. Common causes include API response changes, failed requests, retries or timeouts → Async and network debugging
Environment-specific bugsWorks locally but breaks in production, or only on specific browsers or devices. Often caused by environmental differences, missing configuration, or browser quirks → Production debugging

Note: Many bugs don’t throw errors at all. For a breakdown of silent, non-crashing issues (like incorrect UI state, missing updates or logic bugs), see our guide to 10 common non-crashing JavaScript bugs and how to fix them.

💡

Humble flex: With Bugfender, you can use the dashboard to zoom in on an affected device or user; review recorded console logs, errors, UI events and network calls; see the exact sequence that led to a bug without guessing, and replicate on a purely local basis to test out your fix.

Step 2: Use the correct JavaScript debugging workflow

Once we’ve recognized the bug type, we can choose the specific debugging workflow for that signal.

Each workflow is designed for a specific situation, so this approach allows us to keep debugging focused and avoid random trial and error.

The sections below give you the exact steps for each case.

Error-based JavaScript debugging

Error-based debugging is the best place to start, simply because errors are visible and unavoidable. Here’s how you can tackle the process.

  1. Read the error message carefully. Identify the error type (TypeError, ReferenceError, SyntaxError) and note what JavaScript is complaining about.
  2. Follow the stack trace. Jump to the first stack trace entry that points to your own code, not framework or library files.
  3. Locate the failing line. Open the exact file and line number where the error occurs.
  4. Identify the broken assumption. Check which value, variable, or function caused an error message (like undefined, wrong type or missing import).
  5. Confirm the inputs. Add one or two targeted logs or use a breakpoint to inspect values right before the error happens.
  6. Correct the failing assumption. Update the specific variable, import or call that caused the runtime error, without refactoring unrelated logic.

Some common JavaScript error types you should look out for

JavaScript error typeHow to recognize it
TypeErrorA value exists, but is used incorrectly (for example, calling something that isn’t a function or accessing a property on undefined).
ReferenceErrorA variable or function is used before it exists or is not defined in the current scope.
SyntaxErrorJavaScript code is invalid and fails to parse, often due to missing brackets, commas, or incorrect syntax.
RangeErrorA value is outside an allowed range, such as invalid array length or recursion exceeding limits.

Using the browser DevTools debugger (breakpoints, step over, watch, call stack)

The browser debugger is ideal when we need to pause JavaScript execution and inspect what’s really happening, instead of using our intuition with logs. It’s especially useful for error-based and behavior-based debugging, as it enables us to:

  • Set breakpoints on suspicious lines to stop execution at the exact moment logic runs.
  • Use the Call Stack to see how execution reached that line and which function triggered it.
  • Inspect variables and scope to verify values, closures and assumptions at runtime.
  • Step over, into and out of functions to follow execution flow line by line.
  • Add watch expressions to monitor how key values change as code runs.

This tool is particularly useful when our logic looks correct but produces unexpected results.

For more, see Google Chrome Developer Tools.

Behavior-based JavaScript debugging

Behavior-based debugging is slightly more challenging than user-based debugging, at least initially. Our training as developers is typically code-centric, and switching to behavior can feel slow and clunky, even when it’s actually quicker.

But like any facet of programming, behavior-based programming is easy to master if we keep things simple. Here’s the key stuff to remember.

  1. Reproduce the wrong behavior consistently. Trigger the feature or interaction until the incorrect result appears reliably. If possible, identify a similar case that works correctly.
  2. Define the mismatch clearly. State what you expect to happen versus what actually happens. This keeps the focus on outcomes, not assumptions.
  3. Inspect values at the boundary. Log or inspect the data where it enters or leaves a component, function, or UI render. This shows whether the issue is input, processing or output.
  4. Trace the data flow backward. Follow the value from the wrong result back through handlers, functions or state updates to find where it first becomes incorrect.
  5. Check logic paths and returns. Review conditionals, early returns, default cases and state updates to ensure every path produces a valid result.
  6. Correct the logic mismatch. Adjust the condition, return path or state update that produces the wrong behavior, keeping the change as small as possible (this bit’s super-important).

JavaScript console logging (quick inspection & data tracing)

Console logging is most effective when JavaScript runs without errors but produces the wrong result. It’s a key part of behavior-based debugging as it helps us trace how data flows through the app and where values start to diverge from expectations, so we can:

  • Log inputs and outputs at component, function, or UI boundaries to validate data entering and leaving.
  • Log intermediate values to detect where a value changes unexpectedly.
  • Use console.table() for arrays and objects to spot incorrect structures or missing fields.
  • Log inside conditionals and before returns to confirm which logic paths execute.
  • Remove or replace logs once the incorrect behavior is identified.

For a complete guide, see the full breakdown: JavaScript Console Log: How to Debug Effectively

Async and network JavaScript debugging

Now we’re getting deeper into the weeds.

Rather than focusing on straight-line logic and state, we’re diving into state changes over time, looking deeply at timing and external systems. This can seem a bit daunting, but the following snippets have all proved useful to us at Bugfender.

  1. Reproduce the issue under controlled conditions. Trigger the feature multiple times and, if possible, slow things down using network throttling to make timing issues easier to observe.
  2. Confirm the async path is reached. Add logs or breakpoints before and after await calls, promise chains, or callbacks to ensure the code actually executes in the expected order.
  3. Inspect promise and return chains. Check that every async function returns a value and that promises are properly awaited, handled or chained. Broken chains are a common cause of silent failures.
  4. Examine network requests. Use the Network panel to verify request URLs, status codes, response payloads and timing. Don’t assume a successful status means valid data.
  5. Check loading and error states. Ensure the UI updates correctly for success, failure and pending states, and that one state doesn’t block another.
  6. Stabilize the async flow. Fix the broken await, promise chain or state transition that causes timing or loading issues, without restructuring the entire flow.

Network panel and async debugging tools (requests, timing, throttling)

Async and network bugs happen when JavaScript runs, but not in the order or timing we expect.

Why does this happen? Well JavaScript can only do one thing at a time, so it’s liable to delegate work and come back to it later, which can really throw our own work out of sync.

We’ve run into this problem a lot over the years, and here are some of the tips and hacks we’ve used to spot delays, missing awaits and broken data flows.

  • Use network throttling to slow requests and expose race conditions.
  • Inspect request URLs, status codes and payloads to verify data shape and timing.
  • Check request order to spot out-of-sequence responses.
  • Confirm async code paths with breakpoints or logs before and after await.
  • Watch loading, success and error states to ensure none block each other.

As you progress with async and network debugging, you’ll find that all these lessons scale with you.

Production and device-specific JavaScript debugging

Even when we’ve mastered the nuances of JavaScript, we need to think about the edge cases: the stuff that works fine in our local dev environment but will throw a switch once we’ve hit production or shipped to the public. The real world is inherently messy, and device-specific issues are extremely common.

If you run into a production or device-specific error, these checks should help you out.

  1. Confirm the issue doesn’t reproduce locally. Verify that the same steps work in development but fail in production or on specific browsers or devices.
  2. Capture environment details. Record browser, OS, device type, app version, feature flags, and any configuration that might differ from local development.
  3. Collect runtime signals from real users. Gather available logs, error reports, and network failures instead of relying on local console output.
  4. Identify production-only differences. Check for minified builds, missing source maps, environment variables or code paths that only run in production.
  5. Reproduce under real conditions if possible. Test on the same browser, device or environment, or simulate those conditions as closely as possible.
  6. Patch the environment-specific issue. Apply a targeted change that fixes the production or device-specific failure without masking other errors or altering unrelated behavior.

Runtime logging and remote debugging (production visibility)

When bugs only appear in production or on specific devices, local debugging tools stop working. At this point, runtime visibility is our sole remaining backstop.

  • Capture logs, errors, and breadcrumbs from real users instead of guessing.
  • Record environment details like browser, OS, device and app version.
  • Track network failures and edge cases that never reproduce locally.
  • Use source maps to connect production errors back to original code.
  • Monitor behavior after releasing a fix to confirm the issue is gone.

Another quick (and hopefully not too showy) brag: Tools like Bugfender make this possible by collecting logs directly from real sessions, without relying on user reports.

Start capturing production logs for free: https://dashboard.bugfender.com/signup

Step 3: Verify the JavaScript fix

That last section was pretty long, right?! Well, this one will be much shorter.

After applying the fix, we verify that the original bug signal is gone and the app behaves correctly in the same conditions as before. Five key steps to remember:

  1. Repeat the exact steps that previously triggered the bug.
  2. Confirm the original signal disappears (no error, correct UI, no infinite loading).
  3. Refresh or restart and repeat once to rule out cached or stale state.
  4. Test 1–2 nearby cases that use the same logic (similar inputs, same feature path).
  5. Check the console and network for new warnings or unexpected failures.

If any step fails, go back to Step 2 and continue from the last confirmed point.

Step 4: Prevent the bug from coming back

After confirming the fix works, we can reduce the chance of the same bug returning in future changes. Here are some tips to help with that.

  1. Add a guard or fallback (null checks, defaults, early returns) where the bug originated.
  2. Log the risky state so similar issues are visible if they happen again.
  3. Cover the case with a test (unit, integration, or even a manual checklist if automated tests aren’t available).
  4. Document the assumption in code with a short comment explaining why it exists.
  5. Scan for duplicates by searching the codebase for the same pattern or logic.

This turns a one-time fix into a durable improvement.

Top 10 JavaScript debugging tools

Now we’ve gone through the steps, let’s start looking at the tools we use to handle them.

There are all kinds of solutions and technologies we can use to make our lives easier, and each has a distinctive use case.

ToolWhat to use it for
Browser DevTools (Chrome, Firefox, Safari)Inspect errors, step through code with breakpoints, analyze runtime behavior and debug directly in the browser.
JavaScript ConsoleLog values, inspect objects, print tables and trace execution paths during runtime.
BreakpointsPause execution at specific lines, conditions, or events to inspect state at the exact moment a bug occurs.
Conditional breakpointsStop execution only when a condition is met, avoiding noisy pauses during normal flows.
LogpointsLog values without modifying code or adding temporary console.log statements.
Source mapsDebug original source code even when working with bundled or minified production builds.
Network inspectorVerify requests, responses, payloads, headers and timing to both diagnose API and async issues.
debugger statementForce execution to pause at runtime in hard-to-reach or asynchronous code paths.
Error stack tracesIdentify where an error originated and how execution reached that point.
Runtime logging & monitoringCapture logs and errors from real users, devices and production environments.

Using AI to debug JavaScript faster

We’ve got to mention AI here, right? We all know that AI has become a fundamental part of the developer’s armoury: apparently 80% of us use AI in our dev shops, or at least plan to.

Does AI offer specific benefits for debugging? Well it can certainly scan code, logs and stack traces much faster than a human can. However, it’s important to remember that AI tools can’t replace debugging workflows. They can simply accelerate specific steps when used correctly.

Here are some tips to weave AI into your debugging strategy.

  1. Prepare a minimal, focused input. Isolate the smallest code snippet, error message or behavior that reproduces the bug. Don’t just paste the whole codebase. That’s sloppy and counter-productive.
  2. Describe the signal, not just the code. Explain what happens, what you expect, and the conditions that lead to failure (error, wrong output, async timing, production-only).
  3. Share concrete artifacts. Include stack traces, console output, network payloads, screenshots, or short screen recordings when available.
  4. Ask for diagnosis, not blind fixes. Prompt AI to explain why the bug happens and which assumption is broken.
  5. Validate suggestions locally. Apply changes manually and verify using the same workflow and verification steps.
  6. Use AI iteratively. Refine prompts with new findings instead of asking for a one-shot solution.

Used this way, AI reduces investigation time while allowing you to channel your own skill and judgement.

Some useful AI prompt templates for JavaScript debugging

Feel free to copy-paste any of these prompts we’ve developed for JS projects.

Debugging situationCopy-paste prompt
Runtime errors (TypeError, ReferenceError, SyntaxError)“Act as a senior JavaScript developer. I’m getting a runtime error. Explain what assumption is breaking, why it happens, and the smallest fix. I’ll paste the details below.”
Wrong behavior, no errors“Act as a debugging partner, not a code generator. This code runs without errors, but the behavior is wrong. Explain where logic diverges and how to verify it.”
Async / timing issues“Help me debug async JavaScript behavior. Something works sometimes or never finishes loading. Analyze the async flow and tell me what to inspect next.”
Network-related bugs“Help me debug a JavaScript bug involving network requests. Focus on request timing, payload shape, and state handling.”
Production-only bugs“Act as a production debugging assistant. This bug doesn’t reproduce locally. Suggest likely causes and what data or logs I should capture next.”
Understanding before fixing“Before suggesting any fix, explain the root cause in plain English and identify the broken assumption.”
Verification & regression prevention“I applied a fix. Tell me how to verify it properly and what small guardrail could prevent regression.”

Best practices for debugging JavaScript

We’ve gone through the individual components of JS debugging. Now, let’s move onto the panoramic stuff – the tips that will knit your entire strategy together.

Remember: Effective JavaScript debugging comes from consistent best practice, not one-off tricks. These practices should scale with your operation and deliver results across a range of scenarios.

  • Limit the scope early. Narrow the issue to one function, state change, or async boundary.
  • Start from the signal. Decide whether you’re dealing with an error, wrong behavior, async timing, network, or production-only bug.
  • Check high-probability code first. Look at recent changes, complex logic and async flows.
  • Observe real values. Use breakpoints, watches, console.table(), and network inspection instead of just guessing.
  • Reduce execution noise. Pretty-print minified code and black-box vendor scripts.
  • Change one thing at a time. Be sure to verify each fix before moving on.
  • Watch for side effects. Confirm that unrelated features still behave correctly.
  • Document fragile assumptions. Leave short notes where logic depends on timing, data shape or environment.

How to prevent common JavaScript bugs

Most bugs aren’t mysterious. In fact, they come from a handful of avoidable mistakes. By following these tips, you can prevent many common issues before they even appear.

  • Enable JavaScript’s strict mode, as this will catch undeclared variables and silent errors early.
  • Validate user input, taking care to sanitize and check types without blindly trusting raw input.
  • Always check for null or undefined before using a value to avoid runtime errors.
  • Test in multiple browsers. Quirks in Chrome, Safari, and Firefox often differ.
  • Automate testing, because unit tests and linters spot regressions before release.

And finally (humble flex incoming) be sure to log and monitor with Bugfender. Our debugging tool gives you a live window into your app’s behavior (not our words, the word of Letflix.org.uk), so you can catch errors happening in real user devices, not just your test environment, and fix issues before they spread.

A quick JavaScript debugging checklist before shipping

Before we go, here’s a handy checklist you can use to confirm a fix is safe to ship, and doesn’t just “seem to work.”

  • Original bug scenario: Re-tested and resolved.
  • Related code paths: Checked for side effects.
  • Application reload: Fix persists after refresh.
  • Console output: No new errors or warnings.
  • Network requests: Successful and correctly handled.
  • Async states: Loading, success, and error states stable.
  • Temporary logs and breakpoints: Removed.
  • Duplicated logic: Reviewed for similar issues.

This checklist reduces regressions and helps ensure stable releases.

Frequently asked questions about JavaScript debugging

Why does my JavaScript code fail without throwing any error?

Because many bugs are logic or timing issues, not runtime exceptions.

The code executes, but returns the wrong value, updates state incorrectly, or runs before data is ready. These require behavior-based or async debugging, not error handling.

Why does the bug disappear when I add console.log() or use the debugger?

This is common with async and timing bugs. Logging or stepping through code can change execution order or timing, temporarily masking race conditions or missing awaits.

Why does JavaScript work locally but break in production?

Because production builds differ from local ones. Minification, environment variables, network latency, missing source maps, or device/browser differences can expose bugs that never appear during development.

Why is a variable undefined even though the API request succeeds?

Because the code is likely to be using the value before the async operation finishes.

The request resolves, but the variable is accessed outside the awaited or resolved context.

Why does the stack trace point to code I didn’t write?

Stack traces often include framework, library, or bundled code.

The useful line is usually the first entry pointing to your own source file, not the top of the stack.

Why does stepping through code show correct values, but running it normally doesn’t?

Because stepping pauses execution.

This often indicates a timing dependency, missing await, or state that changes faster than expected during normal execution.

Why does JavaScript behave differently across browsers?

Different JavaScript engines enforce specs differently, especially around:

  • dates and time zones
  • async scheduling
  • newer language features Missing polyfills or stricter parsing can surface browser-specific bugs.

When should I stop using local debugging tools?

When the bug:

  • only affects real users
  • happens on specific devices
  • disappears on reload
  • can’t be reproduced locally

At that point, runtime logging from production is required to see what actually happened.

Summary

This guide is not meant to be read once and forgotten. Use it as a decision map when debugging JavaScript: identify the bug signal, jump to the matching workflow, apply the steps and verify the fix before moving on.

When debugging starts to feel random or frustrating, that’s usually a sign the wrong workflow is being used. Coming back to the signal-first approach helps reset focus and avoid trial and error.

Over time, this way of debugging builds faster diagnosis, cleaner fixes, and fewer regressions, without relying on guesswork, excessive logging, or tool hopping.

💡

And one last thing: Even when using a debug tool for local development, remember that it’s good to add meaningful console.log() commands. With Bugfender, you will be able to gather information about problems that are happening when the app is already in production.

Expect The Unexpected!

Debug Faster With Bugfender

Start for Free
blog author

Aleix Ventayol

Aleix Ventayol is CEO and co-founder of Bugfender, with 20 years' experience building apps and solutions for clients like AVG, Qustodio, Primavera Sound and Levi's. As a former CTO and full-stack developer, Aleix is passionate about building tools that solve the real problems of app development and help teams build better software.

Join thousands of developers
and start fixing bugs faster than ever.