JavaScript Debugging: The Complete Guide for 2026

Published: February 12, 2026 · 25 min read

Every developer spends more time debugging than writing code. The difference between a junior and senior developer is not how few bugs they produce — it is how fast they find and fix them. This guide covers every JavaScript debugging technique you need, from basic console.log to advanced memory profiling and mobile debugging.

Whether you are tracking down a subtle closure bug, investigating a memory leak in production, or profiling render performance, this guide has you covered with practical examples you can use immediately.

1. Console Methods Beyond console.log

Most developers only use console.log(), but the Console API has a rich set of methods that make debugging far more efficient. Learning these will save you hours of scrolling through unstructured output.

console.log, console.warn, console.error

// Basic logging with different severity levels
console.log('Info:', data);        // Standard output
console.warn('Warning:', issue);   // Yellow warning icon
console.error('Error:', err);      // Red error with stack trace

// String substitution (useful for formatting)
console.log('User %s has %d items', 'Alice', 42);
console.log('Object: %o', { name: 'Alice', role: 'admin' });

// Styled console output (works in browsers, not Node.js)
console.log(
  '%cIMPORTANT%c This message stands out',
  'background: #e74c3c; color: white; padding: 2px 6px; border-radius: 3px;',
  'color: inherit;'
);

console.table

// Display arrays and objects as formatted tables
const users = [
  { name: 'Alice', role: 'admin', active: true },
  { name: 'Bob', role: 'editor', active: false },
  { name: 'Carol', role: 'viewer', active: true }
];

console.table(users);
// Renders a sortable table with columns: (index), name, role, active

// Show only specific columns
console.table(users, ['name', 'role']);

console.group and console.groupCollapsed

// Group related log messages together
console.group('User Authentication');
console.log('Checking credentials...');
console.log('Token valid:', true);
console.log('Permissions:', ['read', 'write']);
console.groupEnd();

// Collapsed by default (click to expand)
console.groupCollapsed('API Response Details');
console.log('Status:', 200);
console.log('Headers:', responseHeaders);
console.log('Body:', responseBody);
console.groupEnd();

// Nested groups for complex operations
console.group('Database Query');
  console.log('SQL:', query);
  console.group('Results');
    console.table(rows);
  console.groupEnd();
  console.log('Duration:', '45ms');
console.groupEnd();

console.time, console.timeLog, console.timeEnd

// Measure how long operations take
console.time('fetchUsers');
const response = await fetch('/api/users');
console.timeLog('fetchUsers');  // fetchUsers: 142.3ms (intermediate)
const data = await response.json();
console.timeEnd('fetchUsers');  // fetchUsers: 187.5ms (final)

// Multiple timers can run simultaneously
console.time('total');
console.time('step1');
await stepOne();
console.timeEnd('step1');  // step1: 50ms

console.time('step2');
await stepTwo();
console.timeEnd('step2');  // step2: 120ms
console.timeEnd('total');  // total: 170ms

console.trace, console.dir, console.assert

// console.trace() -- prints the current call stack
function processOrder(order) {
  validateOrder(order);
}
function validateOrder(order) {
  console.trace('validateOrder called');
  // Prints: validateOrder called
  //   at validateOrder (app.js:5)
  //   at processOrder (app.js:2)
  //   at main (app.js:10)
}

// console.dir() -- displays object with expandable properties
// Useful for DOM elements (console.log shows HTML, dir shows properties)
const el = document.querySelector('#app');
console.log(el);   // <div id="app">...</div>
console.dir(el);   // div#app { childNodes: [...], classList: ... }

// console.assert() -- only logs when condition is FALSE
console.assert(user !== null, 'User should not be null');
console.assert(items.length > 0, 'Cart is empty:', items);
// No output when the assertion passes -- keeps your console clean

// console.count() -- count how many times a label is hit
function handleClick(buttonId) {
  console.count(buttonId);  // "save: 1", "save: 2", "save: 3"
}
console.countReset('save');  // Reset counter to 0

2. Chrome DevTools & Breakpoints

Chrome DevTools is the most powerful JavaScript debugging tool available. Open it with F12 or Ctrl+Shift+I (Cmd+Option+I on Mac). The Sources panel is where real debugging happens.

Line Breakpoints

Click any line number in the Sources panel to set a breakpoint. When execution hits that line, it pauses and you can inspect every variable in scope, step through code line by line, and examine the call stack.

// Execution will pause BEFORE this line runs
function calculateTotal(items) {
  let total = 0;              // <-- Set breakpoint here (click line number)
  for (const item of items) {
    total += item.price * item.quantity;
  }
  return total;
}
// When paused, hover over any variable to see its value
// Use the Scope panel to see all local and closure variables

Conditional Breakpoints

Right-click a line number and select "Add conditional breakpoint." The breakpoint only triggers when your expression evaluates to true. This is essential when debugging loops or functions called thousands of times.

// Right-click line number > "Add conditional breakpoint"
// Condition: item.price < 0
function processItems(items) {
  for (const item of items) {
    applyDiscount(item);  // Only pauses when item.price < 0
  }
}

// Logpoints: right-click > "Add logpoint"
// Logs a message without pausing execution
// Expression: 'Processing item:', item.id, 'price:', item.price
// This is like console.log() but without modifying your code

DOM Breakpoints

In the Elements panel, right-click any DOM node and select "Break on" to pause when the DOM changes.

// DOM breakpoint types:
// 1. Subtree modifications -- pauses when children are added/removed
// 2. Attribute modifications -- pauses when attributes change
// 3. Node removal -- pauses when this element is removed

// Useful for debugging:
// - Elements appearing/disappearing unexpectedly
// - CSS classes being toggled by unknown code
// - Third-party scripts modifying your DOM

XHR/Fetch Breakpoints

In the Sources panel, expand "XHR/fetch Breakpoints" and add a URL pattern. Execution pauses whenever a network request matches that pattern.

// In Sources > XHR/fetch Breakpoints > click "+"
// Enter URL pattern: /api/users
// Now any fetch('/api/users/...') call will pause execution

// This helps you find:
// - Which code is making unexpected API calls
// - Where request parameters are being set
// - The call stack leading to a network request

Event Listener Breakpoints

In Sources panel, expand "Event Listener Breakpoints" to pause on specific browser events like click, keydown, scroll, or load. You do not need to know which code handles the event.

Stepping Through Code

// When paused at a breakpoint, use these controls:
//
// F8 or Resume     -- Continue to next breakpoint
// F10 or Step Over -- Execute current line, move to next
// F11 or Step Into -- Enter the function on current line
// Shift+F11 or Step Out -- Run to end of current function
//
// Watch Expressions: Add expressions to monitor
//   e.g., "items.length", "total > 100", "user?.name"
//
// Call Stack panel: Click any frame to see that function's scope
// Scope panel: Shows Local, Closure, and Global variables

3. The debugger Statement

The debugger statement is a programmatic breakpoint. When DevTools is open, it pauses execution exactly like a breakpoint. When DevTools is closed, it is ignored.

// Pause here when DevTools is open
function processPayment(amount) {
  if (amount < 0) {
    debugger;  // Pauses so you can inspect why amount is negative
  }
  return chargeCard(amount);
}

// Conditional debugging
function handleEvent(event) {
  if (event.type === 'click' && event.target.id === 'submit') {
    debugger;  // Only pause for this specific event
  }
  processEvent(event);
}

// Useful in callbacks where setting breakpoints in DevTools is awkward
fetch('/api/data')
  .then(res => res.json())
  .then(data => {
    debugger;  // Pause to inspect the response data
    renderData(data);
  });

// IMPORTANT: Remove debugger statements before committing!
// Add a lint rule to catch them:
// ESLint: "no-debugger": "error"

4. Stack Traces & Call Stacks

A stack trace shows the sequence of function calls that led to the current point of execution. Reading stack traces correctly is a fundamental debugging skill.

// When an error is thrown, the stack trace shows the call chain
function fetchUser(id) {
  throw new Error('User not found: ' + id);
}
function loadProfile(id) {
  return fetchUser(id);
}
function init() {
  loadProfile(42);
}
init();

// Error: User not found: 42
//   at fetchUser (app.js:2:9)       <-- Error originated here
//   at loadProfile (app.js:5:10)    <-- Called by loadProfile
//   at init (app.js:8:3)            <-- Called by init
//   at app.js:10:1                  <-- Top-level call

// Reading stack traces: start from the TOP (most recent call)
// The first line is WHERE the error happened
// Lines below show the path that got you there

Creating Better Stack Traces

// Name your functions for readable stack traces
// BAD: anonymous functions produce unhelpful traces
element.addEventListener('click', function() {  // "anonymous"
  setTimeout(function() {                        // "anonymous"
    fetch('/api').then(function() {               // "anonymous"
      throw new Error('Something broke');
    });
  }, 100);
});

// GOOD: named functions make the trace self-documenting
element.addEventListener('click', function handleClick() {
  setTimeout(function delayedFetch() {
    fetch('/api').then(function processResponse() {
      throw new Error('Something broke');
    });
  }, 100);
});

// Error.captureStackTrace (V8 / Node.js) for cleaner traces
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    Error.captureStackTrace(this, AppError);
  }
}

5. Debugging Async Code

Asynchronous code is notoriously hard to debug because execution jumps between the event loop, microtask queue, and your code. Modern DevTools makes this manageable with async stack traces. See our Promises & Async/Await Guide for a deep dive into async patterns.

Debugging Promises

// Set breakpoints inside .then() and .catch()
fetchUser(1)
  .then(user => {
    console.log('Resolved:', user);  // Breakpoint here
    return fetchPosts(user.id);
  })
  .catch(err => {
    console.error('Rejected:', err); // Breakpoint here
  });

// Chrome DevTools: enable "Async" checkbox in Call Stack panel
// This shows the FULL chain of async calls, not just the current one

// Debugging tip: log the promise state
const promise = fetchData();
console.log(promise);  // Shows: Promise {<pending>}
// After resolution:    Promise {<fulfilled>: data}
// After rejection:     Promise {<rejected>: Error}

Debugging async/await

// Breakpoints work naturally with async/await
async function loadDashboard() {
  try {
    const user = await fetchUser(1);       // Set breakpoint here
    const posts = await fetchPosts(user.id); // Or here
    render(user, posts);
  } catch (err) {
    console.error('Dashboard failed:', err);
    // DevTools shows the async call stack across await boundaries
  }
}

Debugging setTimeout and setInterval

// The async stack trace in DevTools shows the setTimeout origin
setTimeout(function delayedInit() {
  // Set a breakpoint here.
  // The Call Stack panel (with Async enabled) shows:
  //   delayedInit           <-- current function
  //   (async)
  //   scheduleInit          <-- where setTimeout was called
  //   main
  init();
}, 1000);

6. Network Debugging

The Network tab in DevTools shows every HTTP request your page makes. Use it to debug API calls, verify request payloads, check response codes, and identify slow or failing requests.

// Debugging fetch requests in code
async function debuggableFetch(url, options = {}) {
  console.group(`Fetch: ${options.method || 'GET'} ${url}`);
  console.log('Options:', options);
  console.time('request');

  try {
    const response = await fetch(url, options);
    console.log('Status:', response.status, response.statusText);
    console.log('Headers:', Object.fromEntries(response.headers));

    // Clone the response so we can read it AND pass it on
    const clone = response.clone();
    const body = await clone.text();
    console.log('Body:', body.substring(0, 500));
    console.timeEnd('request');
    console.groupEnd();
    return response;
  } catch (err) {
    console.error('Network error:', err.message);
    console.timeEnd('request');
    console.groupEnd();
    throw err;
  }
}

// Network tab tips:
// - Filter by type: XHR, Fetch, JS, CSS, Img, etc.
// - Filter by text: type a URL fragment in the filter bar
// - Right-click a request > "Copy as fetch" to reproduce it
// - Check the Timing tab to see DNS, TCP, TLS, TTFB, download
// - Enable "Preserve log" to keep requests across page navigation
// - Use "Throttling" dropdown to simulate slow connections

Network Tab Key Features

// In DevTools Network tab, click any request to see:
//   - Headers tab: request and response headers
//   - Payload tab: POST body, query parameters
//   - Preview tab: formatted response (JSON, HTML, images)
//   - Response tab: raw response text
//   - Timing tab: detailed breakdown of request phases
//   - Initiator tab: call stack that triggered the request

7. Memory Leaks

Memory leaks cause your app to slow down over time, eventually crashing the browser tab. The Memory tab in DevTools provides three tools: Heap Snapshot, Allocation Timeline, and Allocation Sampling.

Common Leak Patterns

// LEAK 1: Event listeners never removed
function setupHandler() {
  const data = new Array(10000).fill('x');  // Large allocation
  window.addEventListener('resize', function onResize() {
    console.log(data.length);  // Closure holds reference to data
  });
  // If setupHandler is called repeatedly, listeners accumulate
}
// FIX: Store and remove the listener
let resizeHandler;
function setupHandler() {
  if (resizeHandler) window.removeEventListener('resize', resizeHandler);
  const data = new Array(10000).fill('x');
  resizeHandler = () => console.log(data.length);
  window.addEventListener('resize', resizeHandler);
}

// LEAK 2: Detached DOM nodes
function showNotification(message) {
  const el = document.createElement('div');
  el.textContent = message;
  document.body.appendChild(el);
  setTimeout(() => el.remove(), 3000);
  return el;  // Reference keeps detached DOM node alive!
}
// FIX: do not store references to temporary DOM elements

// LEAK 3: Closures holding large objects
function createProcessor() {
  const cache = {};  // Grows without bound
  return function process(key, data) {
    cache[key] = data;  // Never cleaned up
    return transform(data);
  };
}
// FIX: use WeakMap, set max cache size, or clear periodically

Using Heap Snapshots

// Steps to find a memory leak:
// 1. Open DevTools > Memory tab
// 2. Select "Heap snapshot" and click "Take snapshot"
// 3. Perform the action you suspect is leaking
// 4. Take another snapshot
// 5. Select the second snapshot, then change view to "Comparison"
// 6. Sort by "# Delta" to find objects that grew

// Look for:
// - "(string)" -- growing string storage
// - "Array" -- arrays that never shrink
// - "Detached HTMLDivElement" -- DOM nodes removed but referenced
// - "(closure)" -- closures holding large scopes

// Programmatic memory monitoring:
if (performance.memory) {
  setInterval(() => {
    const { usedJSHeapSize, totalJSHeapSize } = performance.memory;
    const mb = (usedJSHeapSize / 1048576).toFixed(1);
    console.log(`Heap: ${mb} MB`);
  }, 5000);
}

8. Performance Profiling

The Performance tab records everything the browser does: JavaScript execution, layout, paint, and compositing. Flame charts visualize where time is spent.

Recording a Profile

// Steps:
// 1. Open DevTools > Performance tab
// 2. Click the Record button (or Ctrl+E)
// 3. Perform the slow action
// 4. Click Stop
// 5. Examine the flame chart

// Flame chart reading tips:
// - X-axis = time, Y-axis = call stack depth
// - Wider bars = more time spent
// - Yellow = JavaScript, Purple = Layout, Green = Paint
// - Click any bar to see the function, file, and line number
// - Look for wide bars (bottlenecks) and tall stacks (deep calls)

// Programmatic profiling with the User Timing API
performance.mark('render-start');
renderComplexComponent();
performance.mark('render-end');
performance.measure('render-time', 'render-start', 'render-end');

const [entry] = performance.getEntriesByName('render-time');
console.log(`Render took ${entry.duration.toFixed(2)}ms`);

Layout Thrashing

// Layout thrashing: reading layout, then writing, then reading again
// Forces the browser to recalculate layout repeatedly

// BAD: triggers layout on every iteration
const items = document.querySelectorAll('.item');
items.forEach(item => {
  const height = item.offsetHeight;          // READ (triggers layout)
  item.style.height = (height * 2) + 'px';  // WRITE (invalidates layout)
  // Next iteration's read forces a new layout calculation
});

// GOOD: batch reads, then batch writes
const heights = Array.from(items).map(item => item.offsetHeight); // READ all
items.forEach((item, i) => {
  item.style.height = (heights[i] * 2) + 'px';  // WRITE all
});

// Use requestAnimationFrame for smooth DOM updates
function smoothUpdate(element, newHeight) {
  requestAnimationFrame(() => {
    element.style.height = newHeight + 'px';
  });
}

9. Error Handling Patterns

Good error handling is not just about catching errors — it is about catching them at the right level and giving yourself enough information to debug them later.

try/catch

// Catch and handle specific error types
try {
  const data = JSON.parse(userInput);
  processData(data);
} catch (err) {
  if (err instanceof SyntaxError) {
    console.error('Invalid JSON:', err.message);
    showUserError('Please enter valid JSON');
  } else if (err instanceof TypeError) {
    console.error('Data processing error:', err.message);
    showUserError('Unexpected data format');
  } else {
    throw err;  // Re-throw unknown errors
  }
}

// Always include context in error messages
try {
  await saveUser(user);
} catch (err) {
  // BAD: generic message
  console.error('Save failed');
  // GOOD: includes context for debugging
  console.error(`Failed to save user ${user.id}:`, err.message, { user });
}

Global Error Handlers

// Catch uncaught errors (last line of defense)
window.onerror = function(message, source, lineno, colno, error) {
  console.error('Uncaught error:', {
    message,
    source,     // File URL
    lineno,     // Line number
    colno,      // Column number
    stack: error?.stack
  });
  // Send to error tracking service
  reportError({ message, source, lineno, colno, stack: error?.stack });
  return true;  // Prevent default browser error handling
};

// Catch unhandled Promise rejections
window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled promise rejection:', event.reason);
  reportError({
    message: event.reason?.message || String(event.reason),
    stack: event.reason?.stack,
    type: 'unhandledrejection'
  });
  event.preventDefault();
});

// Resource loading errors (images, scripts, stylesheets)
window.addEventListener('error', event => {
  if (event.target !== window) {
    console.error('Resource load failed:', event.target.src || event.target.href);
  }
}, true);  // capture phase catches resource errors

10. Source Maps

Source maps map minified, transpiled, or bundled code back to your original source files. Without them, debugging production code is nearly impossible.

// Source maps are generated by your build tool:

// Webpack (webpack.config.js):
module.exports = {
  devtool: 'source-map',  // Full source maps (production)
  // or: 'eval-source-map'   // Fast rebuild (development)
  // or: 'hidden-source-map' // Source map without reference in bundle
};

// Vite (vite.config.js):
export default {
  build: {
    sourcemap: true    // Generate .map files
  }
};

// esbuild:
// esbuild app.js --bundle --sourcemap --outfile=out.js

// The generated code includes a comment pointing to the map:
// //# sourceMappingURL=app.js.map

// Chrome DevTools loads source maps automatically
// You will see your original files in the Sources panel
// Breakpoints, stepping, and variable names all work
// on the original code, not the minified version

// Tip: upload source maps to your error tracking service
// (Sentry, Bugsnag) and use "hidden-source-map" in production

11. Node.js Debugging

Node.js has a built-in inspector that connects to Chrome DevTools or VS Code, giving you the same breakpoint and profiling experience as browser debugging.

// Start Node.js with the inspector
// $ node --inspect server.js
// Debugger listening on ws://127.0.0.1:9229/...
// Open chrome://inspect in Chrome and click "inspect"

// Break on the first line of execution
// $ node --inspect-brk server.js

// VS Code launch.json configuration
{
  "type": "node",
  "request": "launch",
  "name": "Debug Server",
  "program": "${workspaceFolder}/server.js",
  "skipFiles": ["<node_internals>/**"]  // Skip Node core modules
}

// Attach to a running process
{
  "type": "node",
  "request": "attach",
  "name": "Attach to Process",
  "port": 9229
}

// Debug a specific test file
// $ node --inspect-brk node_modules/.bin/jest --runInBand test.js

// Listen for Node.js warnings in development
process.on('warning', warning => {
  console.warn('Node warning:', warning.name, warning.message);
});

12. Common JS Bugs & How to Spot Them

These bugs catch even experienced developers. Understanding the patterns makes them easy to identify and fix. You can test all of these examples in our JavaScript Runner.

Variable Hoisting

// var declarations are hoisted, but not their assignments
console.log(x);  // undefined (not ReferenceError!)
var x = 5;

// The engine sees this as:
var x;             // Declaration hoisted to top
console.log(x);   // undefined
x = 5;            // Assignment stays in place

// let and const are NOT hoisted (temporal dead zone)
console.log(y);  // ReferenceError: Cannot access 'y' before initialization
let y = 5;

// Function declarations ARE fully hoisted
greet();  // Works! Logs "hello"
function greet() { console.log('hello'); }

// Function expressions are NOT hoisted
greet();  // TypeError: greet is not a function
var greet = function() { console.log('hello'); };

Closure Pitfalls

// Classic loop closure bug
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Outputs: 3, 3, 3 (not 0, 1, 2!)
// Because var is function-scoped, all closures share the same i

// FIX 1: Use let (block-scoped, creates new binding per iteration)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Outputs: 0, 1, 2

// FIX 2: IIFE to capture the value (pre-ES6 pattern)
for (var i = 0; i < 3; i++) {
  (function(j) { setTimeout(() => console.log(j), 100); })(i);
}

this Binding

// 'this' depends on HOW a function is called, not where it is defined
const user = {
  name: 'Alice',
  greet() {
    console.log('Hello, ' + this.name);
  }
};

user.greet();           // "Hello, Alice" (method call)
const fn = user.greet;
fn();                   // "Hello, undefined" (this = window/global)

// Common bug: callbacks lose 'this'
class Timer {
  constructor() {
    this.seconds = 0;
  }
  start() {
    // BUG: 'this' inside setInterval is window, not Timer
    setInterval(function() {
      this.seconds++;  // NaN -- window.seconds is undefined
    }, 1000);
  }
}

// FIX 1: Arrow function (inherits this from enclosing scope)
start() {
  setInterval(() => {
    this.seconds++;  // Correct: 'this' is the Timer instance
  }, 1000);
}

// FIX 2: bind()
start() {
  setInterval(function() {
    this.seconds++;
  }.bind(this), 1000);
}

Reference vs Value

// Primitives are copied by value
let a = 5;
let b = a;
b = 10;
console.log(a);  // 5 (unchanged)

// Objects and arrays are copied by REFERENCE
const arr1 = [1, 2, 3];
const arr2 = arr1;
arr2.push(4);
console.log(arr1);  // [1, 2, 3, 4] -- BOTH changed!

// FIX: Create a copy
const arr3 = [...arr1];           // Shallow copy (spread)
const arr4 = structuredClone(arr1); // Deep copy (modern)

// Subtle object mutation bug
function addDefaults(config) {
  config.timeout = config.timeout || 3000;  // Mutates the original!
  return config;
}
const settings = { retries: 3 };
addDefaults(settings);
console.log(settings.timeout);  // 3000 -- settings was mutated

// FIX: spread to create a new object
function addDefaults(config) {
  return { timeout: 3000, ...config };  // Original unchanged
}

Equality Gotchas

// Always use === (strict equality), never == (loose equality)
0 == ''       // true  (type coercion!)
0 == false    // true
'' == false   // true
null == undefined  // true
0 === ''      // false (no coercion)
0 === false   // false

// NaN is not equal to itself
NaN === NaN   // false
Number.isNaN(NaN)  // true (use this instead)

// Object equality checks references, not contents
{} === {}     // false (different objects)
[] === []     // false (different arrays)

// To compare object contents, use JSON or a deep-equal utility
JSON.stringify(obj1) === JSON.stringify(obj2)  // Simple but fragile

13. Mobile Debugging

Bugs that only appear on mobile devices are among the hardest to track down. Fortunately, both Chrome and Safari offer remote debugging capabilities.

Chrome Remote Debugging (Android)

// 1. On your Android device:
//    Settings > Developer Options > Enable USB Debugging
// 2. Connect device to computer via USB
// 3. On desktop Chrome, navigate to: chrome://inspect
// 4. Your device and open tabs appear under "Remote Target"
// 5. Click "inspect" next to any tab

// You get FULL DevTools: Console, Sources, Network, Memory, etc.
// Changes in DevTools reflect on the device in real time
// You can also forward ports for localhost testing:
//   chrome://inspect > Port forwarding > localhost:3000

// Quick mobile debugging without USB:
// Use Chrome DevTools device emulation:
// 1. Open DevTools (F12)
// 2. Click the device toggle (Ctrl+Shift+M)
// 3. Select a device preset or set custom dimensions
// 4. Toggle touch simulation, throttle network and CPU

Safari Web Inspector (iOS)

// 1. On iPhone/iPad: Settings > Safari > Advanced > Web Inspector = ON
// 2. Connect device to Mac via USB
// 3. On Mac Safari: Develop menu > [Your Device] > [Tab]
//    (Enable Develop menu: Safari > Preferences > Advanced)
// 4. Full Web Inspector opens for the mobile page

Mobile-Specific Debugging Tips

// Eruda: inject a mobile console into any page during development
// <script src="https://cdn.jsdelivr.net/npm/eruda"></script>
// <script>eruda.init();</script>

// Debug viewport and touch issues
console.log('Viewport:', window.innerWidth, 'x', window.innerHeight);
console.log('Device pixel ratio:', window.devicePixelRatio);
console.log('Touch support:', 'ontouchstart' in window);

Frequently Asked Questions

What is the fastest way to debug JavaScript?

For quick checks, console.log() remains the fastest approach. For complex bugs, use Chrome DevTools breakpoints: open the Sources panel (Ctrl+Shift+I then Sources tab), click a line number to set a breakpoint, and reload. Execution pauses at that line, letting you inspect every variable in scope, step through code line by line, and examine the call stack. Conditional breakpoints (right-click a line number) are especially powerful because they only pause when a specific condition is true.

How do I debug async JavaScript code like Promises and async/await?

Chrome DevTools has built-in async stack traces that show the full call chain across await boundaries. Enable "Async" in the Call Stack panel. Set breakpoints inside .then() callbacks or after await statements. Use console.trace() inside async functions to log the full async stack. For unhandled Promise rejections, listen for the unhandledrejection event on window. The "Pause on caught exceptions" toggle in the Sources panel is also useful for catching silently swallowed errors in Promise chains.

How do I find and fix memory leaks in JavaScript?

Use Chrome DevTools Memory tab. Take a heap snapshot, perform the action you suspect is leaking, take another snapshot, and compare them to see which objects grew. Look for detached DOM nodes (DOM elements removed from the page but still referenced in JavaScript), growing arrays or maps, and event listeners that were never removed. The Allocation Timeline records memory allocations in real time, helping you pinpoint exactly which function is allocating memory that is never freed.

What is a source map and why do I need it for debugging?

A source map is a file that maps minified or transpiled code back to your original source code. When your JavaScript is bundled and minified for production (e.g., by Webpack, Vite, or esbuild), the code in the browser is unreadable. Source maps let Chrome DevTools show your original files with proper variable names, comments, and formatting. They are loaded automatically when detected. Generate them with your bundler's devtool or sourcemap option, and serve them in development but optionally exclude them from production.

How do I debug JavaScript on a mobile device?

For Android: enable USB debugging on your device, connect via USB, open chrome://inspect in desktop Chrome, and you will see your mobile browser tabs listed. Click "inspect" to open full DevTools for that tab. For iOS: connect your iPhone via USB, enable Web Inspector in Settings > Safari > Advanced, then open Safari on your Mac and select your device under the Develop menu. For quick testing without a physical device, use Chrome DevTools device emulation (Ctrl+Shift+M) to simulate screen sizes, touch events, and throttled network speeds.

Conclusion

Debugging is a skill that improves with practice and knowledge of your tools. The developers who debug fastest are not the ones who write fewer bugs — they are the ones who know exactly which tool to reach for when something goes wrong. console.log for quick checks, breakpoints for complex logic, the Network tab for API issues, Memory tab for leaks, and Performance tab for slowdowns.

Build the habit of using the right tool for each type of bug. Use console.table instead of logging raw arrays. Set conditional breakpoints instead of adding if statements to your code. Use the debugger statement when you need to pause in a callback. These small improvements compound into dramatically faster debugging over time.

⚙ Practice: Test the code examples from this guide in our JavaScript Runner, or clean up minified code with the JS Beautifier before debugging.

Related Resources

Keep learning: Debugging is closely tied to understanding JavaScript fundamentals. Review our ES6+ Features Guide to understand scoping rules and arrow function behavior, and our Promises & Async/Await Guide for mastering async debugging.

Related Tools

JavaScript Runner
Run and test JavaScript code instantly in your browser
JS Beautifier
Format and beautify minified JavaScript for debugging
Promises & Async/Await Guide
Master async JavaScript patterns for better debugging
JavaScript ES6+ Features Guide
Modern JS fundamentals: scoping, closures, and more