Debugging JavaScript: From console.log to Pro (2026)
Every developer spends more time debugging than writing code. The difference between juniors and seniors isn't writing bug-free code — it's finding bugs faster.
Beyond console.log
// You already know these:
console.log('Basic log');
console.error('Error message');
console.warn('Warning message');
// But these are MORE useful:
// Formatted logging (%s=string, %o=object, %d=number, %c=CSS style):
console.log('%c ERROR %c User %s not found',
'color: red; font-weight: bold; background: #ffe0e0; padding: 2px 6px;',
'color: blue;', userId);
// Output: Red "ERROR" badge + blue userId
// Table view for arrays of objects:
const users = [
{ id: 1, name: 'Alice', role: 'admin', active: true },
{ id: 2, name: 'Bob', role: 'user', active: false },
];
console.table(users, ['name', 'role']); // Only show selected columns!
// Grouping related logs:
console.group('User Authentication');
console.log('Step 1: Validate token');
console.log('Step 2: Fetch user');
console.log('Step 3: Check permissions');
console.groupEnd();
// Collapsible in browser DevTools!
// Timing operations:
console.time('API call');
await fetch('/api/data');
console.timeEnd('API call'); // Output: "API call: 243.45ms"
// Assert (log only if condition is false):
function processUser(user) {
console.assert(user.email, 'User must have email:', user); // Shows stack trace!
// ... rest of function
}
// Trace (shows how you got here):
function helper() {
console.trace('Called from:');
}
function main() {
helper(); // Shows full call stack leading to this point
}
// Count (track how many times something runs):
let renderCount = 0;
function render() {
renderCount++;
console.count('render called'); // "render called: 1", "render called: 2", ...
}
Chrome DevTools Debugger
// Instead of adding/removing console.logs, use the debugger statement:
function complexCalculation(a, b) {
const sum = a + b;
debugger; // Pauses execution here! Inspect variables in DevTools.
return sum * 2;
}
// Conditional breakpoint (in DevTools Sources panel):
// Right-click line number → "Add conditional breakpoint"
// Enter: count > 100 → Only pauses when condition is true
// Enter: user.id === 'abc123' → Only pauses for specific user
// Logpoint (non-breaking console.log at breakpoint):
// Right-click → "Add logpoint"
// Enter: 'User ID:', userId → Logs without pausing!
// Great for production debugging where you can't stop execution
// XHR/Fetch breakpoints (DevTools → Sources → XHR breakpoints):
// Set to break on any URL containing "/api/"
// Or break on specific request types
// DOM breakpoints:
// Select element in Elements tab → right-click → Break on:
// - Subtree modifications (when children change)
// - Attribute modifications (when attributes change)
// - Node removal (when element is deleted)
// Invaluable for tracking down rogue JS modifying your DOM!
// Console API tricks available in DevTools:
$0 // Currently selected element in Elements tab
$1 // Previously selected element
$('button') // querySelector alias
$$('.item') // querySelectorAll alias
copy(data) // Copy to clipboard
keys(object) // Show object keys
values(object) // Show object values
monitor(fn) // Log every time fn is called with arguments
monitorEvents(element, 'click') // Log all click events on element
Node.js Debugging
// Built-in inspector (Node.js 6.3+):
// node --inspect app.js
// Then open chrome://inspect in Chrome and connect!
// Or inspect on first line:
// node --inspect-brk app.js
// Debugging with VS Code:
// Create .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Server",
"program": "${workspaceFolder}/src/server.js",
"skipFiles": ["<node_internals>/**"],
"env": { "NODE_ENV": "development" }
},
{
"type": "node",
"request": "attach",
"name": "Attach to Running",
"port": 9229,
"restart": true,
"stopOnEntry": false
},
{
"type": "node",
"request": "launch",
"name": "Run Current File",
"program": "${file}",
"console": "integratedTerminal"
}
]
}
// Useful launch configurations:
// Debug Mocha tests:
{
"type": "node",
"request": "launch",
"name": "Mocha All",
"program": "${workspaceFolder}/node_modules/.bin/mocha",
"args": ["--timeout", "10000", "${workspaceFolder}/test/**/*.js"]
}
// Debug with environment-specific .env:
// Install: npm install dotenv-cli
{
"type": "node",
"request": "launch",
"envFile": "${workspaceFolder}/.env.development",
"program": "${workspaceFolder}/src/server.js"
}
Source Maps & Production Debugging
// Source maps connect your compiled/minified code back to original source:
// tsconfig.json:
{
"compilerOptions": {
"sourceMap": true,
"sourceRoot": "./src",
"inlineSourceMap": false, // Separate .map file (don't inline in prod!)
"mapRoot": "./dist/maps"
}
}
// webpack/vite config:
module.exports = {
devtool: 'source-map', // Best for production (separate file)
// devtool: 'cheap-module-source-map', // Faster rebuilds for dev
};
// NEVER upload source maps to public servers in production!
// They expose your entire source code.
// Options:
// 1. Don't generate source maps in production builds
// 2. Generate but don't deploy them (keep internally for error reporting)
// 3. Use a service that requires authentication to access maps (Sentry, etc.)
// Error tracking with source map support:
// Sentry automatically applies source maps when you upload them:
// npx @sentry/cli sourcemaps upload ./dist
// Now errors show the ORIGINAL code line, not minified code!
Common Bugs & How to Find Them Fast
// Bug #1: "undefined is not a function" / "Cannot read property of undefined"
// Cause chain: variable is undefined → accessing property → crash
// Quick fix: Add optional chaining to find where it breaks:
// Before: data.user.address.city
// After: data?.user?.address?.city
// If this doesn't crash → the problem is somewhere in that chain
// Bug #2: Async/await error swallowed silently
async function broken() {
const data = await fetch('/api'); // If this throws...
processData(data); // ...this never runs, but no error shown!
}
// Fix: Always use try/catch or .catch():
async function fixed() {
try {
const data = await fetch('/api');
processData(data);
} catch (err) {
console.error('Fetch failed:', err); // NOW you see the error!
}
}
// Or add global handler:
process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection:', reason);
});
// Bug #3: Race conditions
let cachedData;
async function getData() {
if (!cachedData) {
cachedData = await slowFetch(); // Two calls arrive simultaneously!
} // Both see null → both fetch!
return cachedData;
}
// Fix: Use promise caching (not value caching):
let fetchPromise;
async function getDataFixed() {
if (!fetchPromise) {
fetchPromise = slowFetch().finally(() => { fetchPromise = null; });
}
return fetchPromise;
}
// Now concurrent calls share the SAME promise!
// Bug #4: Memory leak detection
// In DevTools → Memory tab:
// 1. Take heap snapshot
// 2. Do the operation that might leak
// 3. Take another heap snapshot
// 4. Compare snapshots → objects growing = leak suspects
// Look for "(detached)" elements — DOM nodes referenced by JS but removed from page
// Bug #5: Off-by-one errors in loops
for (let i = 0; i <= array.length; i++) { // ≤ instead of < !
// Crashes on last iteration (array[length] = undefined)
}
// Fix: Use for...of when you don't need index:
for (const item of array) { /* safe */ }
// Or use .forEach(), .map(), .filter() — no index bugs possible
What's your favorite debugging technique? What's the weirdest bug you've ever found?
Follow @armorbreak for more practical developer guides.
Top comments (0)