Every JavaScript developer spends a significant portion of their time debugging. Whether it is tracking down a mysterious undefined value, understanding why an event listener fires twice, or figuring out why an API call returns unexpected data, debugging is as much a part of programming as writing code. The difference between a junior and senior developer often comes down to how efficiently they can diagnose and fix problems — not how quickly they can type.
Most developers default to sprinkling console.log() throughout their code, re-running, and hoping. That works for trivial issues, but it breaks down when the bug only reproduces under specific conditions, when the error originates deep inside third-party code, or when the problem is an asynchronous race that never shows up twice in the same place.
This guide covers eight techniques that will make you significantly more effective — each with a short code example you can copy. We assume you know the basics of opening the browser's developer tools and reading a stack trace. If you work with a teammate while debugging, you may also find our guide on remote pair programming useful for structuring these sessions.
1 Use console.table() for Arrays and Objects
Most developers default to console.log() for everything, but when you are inspecting an array of objects, console.table() displays the data in a sortable, scrollable table format that is dramatically easier to scan. Instead of expanding nested objects one by one, you get a clean tabular view instantly.
const users = [
{ id: 1, name: "Alex", email: "alex@x.com", role: "admin" },
{ id: 2, name: "Priya", email: "priya@x.com", role: "user" },
{ id: 3, name: "Daniel", email: "daniel@x.com", role: "user" },
];
// Hard to scan:
console.log(users);
// Clean tabular view, sortable by column:
console.table(users);
// Pick only the columns you care about:
console.table(users, ["name", "role"]);This is especially useful when inspecting API responses, rows from a database, or any collection where objects share the same shape.
2 Master Conditional Breakpoints
Regular breakpoints pause execution every time a line is hit. This becomes impractical in loops or frequently called functions. Conditional breakpoints solve this by only pausing when a specified condition is true. In Chrome DevTools, right-click a line number in the Sources panel, select "Add conditional breakpoint," and enter an expression.
// Loop over 10,000 orders — regular breakpoint would pause 10,000 times
for (const order of orders) {
// Set a conditional breakpoint here with: order.total > 10000 && order.status === "pending"
processOrder(order);
}This technique is invaluable when debugging production-like data. Pause only on the specific record or state combination causing the bug, skipping thousands of successful iterations. Logpoints (a close cousin) log a value without pausing — ideal when you want instrumentation without editing the source.
3 Use the debugger Statement
Adding the debugger; statement directly in your code creates a programmatic breakpoint. When DevTools is open, JavaScript execution pauses at that line, and you can inspect variables, step through code, and examine the call stack. This is often faster than navigating to the right file in DevTools and setting a breakpoint manually — especially when your code is bundled and the file structure in the Sources panel is unfamiliar.
function calculateTotal(items, discount) {
debugger; // Pauses here when DevTools is open
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
return subtotal * (1 - discount);
}The debugger; statement is ignored when DevTools is closed, so it is safe to use during development. Just remember to remove it before committing — it is easy to forget and accidentally ship a debugger statement to production, where any user with DevTools open will see your application freeze.
4 Read Error Stack Traces Carefully
When JavaScript throws an error, the stack trace tells you exactly which function calls led to the failure. Many developers only read the first line (the error message) and ignore the rest. But the stack trace shows you the chain of function calls that caused the error, which is often more valuable than the error message itself.
TypeError: Cannot read properties of null (reading 'email')
at renderUserCard (UserCard.jsx:17:23) ← your code: the bug is here
at UserList.render (UserList.jsx:42:18) ← your code: the caller
at processChild (react-dom.js:...) ← library internals, ignoreRead from top down: the top frame is where the error was thrown, each line below is the caller. Look for your own file names in the stack rather than library internals. The first frame from your code is usually where the bug is — either you passed wrong arguments, called a function at the wrong time, or failed to handle a null value. In the example above, UserCard.jsx:17 tried to read .email on a null user — the fix is to guard against missing users at that line or in the parent that should have provided one.
5 Inspect Network Requests
Many JavaScript bugs are not in your code at all — they are in the data flowing between your frontend and backend. The Network tab in DevTools shows you every HTTP request your application makes, including the request URL, method, headers, request body, response status, and response body.
When an API call fails or returns unexpected data, check the Network tab first. Common issues include sending the wrong Content-Type header, missing authentication tokens, the server returning a different JSON shape than your code expects, or CORS preflight requests failing silently. Use the "Copy as cURL" option to replay a request from your terminal and isolate whether the problem is in the server response or in how your client code handles it.
6 Use console.trace() to Find Call Origins
When a function is called from multiple places and you need to know which call path is causing a problem, console.trace() prints a stack trace at that point without pausing execution. This is incredibly useful for debugging event handlers, callback chains, and middleware that can be triggered from multiple sources.
function handleSubmit(formData) {
console.trace("handleSubmit called"); // Shows which button triggered it
submitForm(formData);
}Unlike the debugger; statement, it does not pause execution, so it is less intrusive and works well in code that runs frequently. You can add a label to distinguish multiple traces in the same session.
7 Reproduce the Bug Before Fixing It
The most common debugging mistake is trying to fix a bug you cannot reliably reproduce. If you cannot make the bug happen consistently, you cannot verify that your fix actually works. Before writing any fix, invest time in understanding the exact conditions that trigger the bug: specific input values, timing, user actions, or state combinations.
Write a minimal test case that reproduces the problem. Strip away everything that is not related to the bug. This process often reveals the root cause before you even start looking at the code, because you discover which conditions are necessary and which are coincidental. A minimal repro is also the single most valuable artifact to share when asking a teammate for help — it shortcuts hours of context-gathering.
8 Debug Collaboratively
Sometimes the fastest way to find a bug is to explain it to someone else. The act of describing the problem forces you to organize your understanding and often reveals assumptions you have been making. This is known as "rubber duck debugging," but it works even better with a real person who can ask questions and suggest alternative approaches.
Tools like ShareCode make collaborative debugging easy — paste the problematic code into a shared code space, invite a colleague, and walk through it together in real time. Both of you can edit the code, test hypotheses, and see each other's cursors as you work. Fresh eyes on a problem often spot issues in seconds that you have been staring at for hours.
Bonus: Three advanced techniques worth learning
Once the eight basics above are second nature, these three techniques will cover most of the harder cases you run into:
Async stack traces
By default, a stack trace from inside an async function loses the chain of await calls that led to it. In Chrome DevTools, enable Settings → Preferences → Enable async stack traces to restore the full call path. This is the single highest-leverage setting to change in DevTools for most web developers.
Source maps
In production, your code is minified and bundled — a stack trace pointing to main.bundle.js:1:28472 is useless on its own. Source maps translate that back to your original file and line. Most modern bundlers generate source maps automatically; make sure they are deployed (or uploaded to your error-tracking service) and that your browser has "Enable JavaScript source maps" turned on in DevTools settings.
Watch expressions and the Scope pane
While paused at a breakpoint, the Scope pane in DevTools shows every variable in the current closure. Watch expressions let you pin an arbitrary expression (for example, users.filter(u => u.active).length) that is re-evaluated on every step — useful when a derived value is what you care about, not a single variable.
Common debugging mistakes to avoid
- Changing multiple things at once. When you tweak three variables and the bug goes away, you don't know which change fixed it. Change one thing, verify, then move on.
- Trusting the error message literally. JavaScript error messages are often misleading — a
TypeErroris frequently caused by the thing that produced the null value upstream, not the line where the null is read. - Ignoring browser warnings. Yellow warnings in the console about deprecated APIs, hydration mismatches, or unhandled promises are usually pointing at the root cause of a bug that will surface as an error later.
- Not reading the docs. If you're debugging a third-party library, the MDN reference or the library's changelog usually answers the question in 30 seconds.
Building a debugging mindset
The best debuggers are not the ones who know the most tools — they are the ones who approach problems systematically. They form hypotheses, test them methodically, and narrow down the problem space with each step. They resist the urge to make random changes and hope the bug goes away.
Every bug you fix makes you a better developer if you take a moment to understand not just what went wrong, but why your mental model of the code was incorrect. Over time, these lessons compound into an intuition for where bugs hide and how to find them quickly. Keep a short personal log of the non-obvious bugs you solve — after a year, it becomes the most useful notebook you own.
Debugging in production vs. development
The techniques above work best in a local development environment where you have full access to DevTools, source maps, and the ability to add breakpoints. Production debugging is a different discipline because you cannot attach a debugger to a user's browser and you rarely have the exact state that triggered the bug.
In production, structured logging is your primary tool. Log the inputs and outputs of critical functions — API handlers, authentication flows, payment processing — so you can reconstruct the sequence of events that led to the failure. Use a logging service that supports search, filtering, and correlation IDs so you can trace a single user's session across multiple services.
Error-tracking tools like Sentry or Bugsnag automatically capture unhandled exceptions with stack traces, browser metadata, and breadcrumbs (the sequence of UI events before the crash). Deploy source maps to your error tracker so stack traces point to your original source, not minified bundles. For issues that only reproduce in specific environments, feature flags let you toggle code paths without redeploying — useful for isolating which change introduced the regression.
Frequently asked questions
- What is the fastest way to debug a JavaScript error in production?
- Start with the Network tab and the Console. Check for failed requests, then read the error stack trace for your own file names (not library internals). Source maps translate minified stack frames back to your original source. For Node.js, structured logs around the failing code path typically beat any remote debugger.
- Why does my breakpoint not pause execution?
- Usually one of three reasons: the source map points to a file that is not the one actually executing, the code is running in a different context (service worker, iframe, web worker), or the breakpoint is on a blank line. Drop a temporary
debugger;statement to confirm the line actually runs. - What is the difference between console.log, console.debug, and console.trace?
console.logwrites a standard message.console.debugwrites at the Verbose level and is hidden by default unless you enable Verbose logging.console.traceprints a stack trace showing the call path that reached that line.- How do I debug an async function that throws an error?
- Enable async stack traces in DevTools settings. Wrap awaits in
try/catchor attach a.catch()so errors surface with full stack traces rather than becoming unhandled rejections. - Can I debug JavaScript collaboratively with a teammate?
- Yes. ShareCode lets two or more developers edit the same code in real time with color-coded cursors. Paste the problem, share the link, and walk through hypotheses together — often the fastest way to crack a subtle bug.