How to Debug JavaScript Like a Pro
Debugging is a skill. A skill you can learn. Here's my complete debugging toolkit.
The Mindset
❌ "This code doesn't work" (vague, unhelpful)
✅ "When I click submit with an email over 50 chars,
the validation error doesn't show and the form submits anyway"
(specific, reproducible)
The better you describe the bug, the faster you'll fix it.
Step 1: Reproduce the Bug
// Before fixing anything:
// 1. Can you reproduce it consistently?
// 2. What are the EXACT steps?
// 3. What did you EXPECT to happen?
// 4. What ACTUALLY happened?
// 5. What's the error message (if any)?
// Write a reproduction script
// repro.js — run this to see the bug
const input = { email: 'a'.repeat(51) }; // Over 50 chars
const result = validateEmail(input.email);
console.log('Input:', input.email.length, 'chars');
console.log('Result:', result);
console.log('Expected: { valid: false }');
console.log('Match:', result.valid === false ? '✅' : '❌ BUG!');
Step 2: Read the Error Message
// ❌ Ignoring errors
try {
riskyOperation();
} catch (e) {
// empty catch — THE WORST THING YOU CAN DO
}
// ✅ Actually read the error!
try {
riskyOperation();
} catch (e) {
console.error({
name: e.name, // TypeError, ReferenceError, etc.
message: e.message, // What went wrong
stack: e.stack, // Where it happened (line numbers!)
cause: e.cause // Wrapped error (if any)
});
}
// TypeError: Cannot read properties of undefined (reading 'map')
// at Object.processUsers (<anonymous>:15:23)
// at <anonymous>:20:5
// ↑ This tells you: line 15, something is undefined when calling .map()
Step 3: Console Debugging (Quick & Dirty)
// console.log — the basics
function process(data) {
console.log('Input:', data); // What came in?
console.log('Type:', typeof data); // What type is it?
console.log('Length:', data?.length); // Does it have length?
console.log('Keys:', data ? Object.keys(data) : null); // What keys?
return data.map(item => item * 2); // Where error happens
}
// console.table — for arrays/objects
const users = [{name:'Alice',age:30}, {name:'Bob',age:25}];
console.table(users);
// String interpolation with labels
console.log('Before:', JSON.stringify(obj));
doSomething(obj);
console.log('After:', JSON.stringify(obj));
console.log('Changed?', JSON.stringify(before) !== JSON.stringify(after));
// Counters — how many times does this run?
let callCount = 0;
originalFunction = function(...args) {
callCount++;
console.log(`Call #${callCount}:`, args);
return originalFunction.apply(this, args);
};
Step 4: Breakpoints (The Right Way)
// Instead of console.log everywhere, use debugger;
function complexCalculation(a, b, c) {
const step1 = a * b; // Want to see what step1 is?
debugger; // ← Execution pauses here! Open DevTools.
const step2 = step1 + c;
const step3 = Math.sqrt(step2);
return step3;
}
// In Chrome DevTools:
// 1. Open Sources tab
// 2. Find your file
// 3. Click line number to set breakpoint
// 4. Or: add `debugger;` in code
// 5. Run code → execution pauses at breakpoint
// 6. Use Controls:
// - Step Over (F10): Execute next line
// - Step Into (F11): Go into function
// - Step Out (Shift+F11): Exit current function
// - Continue (F8): Run until next breakpoint
// Conditional breakpoints (game changer!)
// Right-click breakpoint → Edit breakpoint → enter condition:
// users.length > 0 // Only pause when array not empty
// id === 123 // Only pause for specific ID
// typeof value === 'string' // Only pause for strings
// i % 100 === 0 // Every 100th iteration
Step 5: Call Stack & Scope
// When paused at breakpoint, check these panels:
// Call Stack panel:
// Shows HOW you got here
// complexCalculation (script.js:15)
// processData (script.js:22)
// handleRequest (script.js:45)
// ← Click any frame to see that function's scope!
// Scope panel:
// Shows ALL variables in current scope
// Local: variables in current function
// Closure: variables from outer functions (closures!)
// Script: global variables
// Block: block-scoped variables (let/const in if/for)
// Watch panel:
// Add expressions to monitor continuously
// - data.length
// - user.role === 'admin'
// - performance.now() - startTime
Step 6: Network Debugging
// Is the problem frontend or backend?
// Check Network tab:
// 1. Did the request fire? (Look for it in list)
// 2. What status code? (200? 404? 500?)
// 3. What was sent? (Headers tab → Request Headers)
// 4. What came back? (Response tab / Preview tab)
// 5. How long did it take? (Time tab / Waterfall)
// Common network issues:
// POST → 400: Bad request body (check Preview of request)
// GET → 401: Missing auth token (check Request Headers)
// PUT → 403: Has token but wrong permissions
// Any → 500: Server error (check server logs, not frontend!)
// Any → CORS: Check if origin is allowed on server side
// Fetch error handling that shows the REAL issue
async function apiCall(url, options) {
try {
const response = await fetch(url, options);
if (!response.ok) {
const body = await response.json().catch(() => ({}));
throw new ApiError(
body.message || response.statusText,
response.status,
body,
url,
options
);
}
return await response.json();
} catch (error) {
if (error instanceof ApiError) {
console.error(`API Error ${error.status}:`, error.message);
console.error('URL:', error.url);
console.error('Request:', error.requestOptions);
console.error('Response body:', error.responseBody);
}
throw error;
}
}
Step 7: Performance Debugging
// Is it slow? Find out WHY.
// Option 1: Performance tab (Chrome DevTools)
// 1. Click Record (⚫)
// 2. Do the slow thing
// 3. Stop recording
// 4. See flame chart of where time was spent
// Option 2: console.time
console.time('bigOperation');
await bigOperation();
console.timeEnd('bigOperation'); // bigOperation: 1234.567ms
// Option 3: Performance API (programmatic)
const start = performance.now();
doSomethingComplex();
const duration = performance.now() - start;
if (duration > 1000) {
console.warn(`Slow operation took ${(duration/1000).toFixed(1)}s`);
}
// Common performance bugs I've found:
// 1. N+1 queries (query inside a loop)
// 2. Re-rendering entire list when one item changes
// 3. Synchronous operations blocking event loop
// 4. Not debouncing search/input handlers
// 5. Loading full dataset instead of paginating
Step 8: Memory Leak Detection
// Memory tab in DevTools:
// 1. Take heap snapshot (before)
// 2. Do the action that might leak
// 3. Take heap snapshot (after)
// 4. Compare snapshots → find objects that grew
// Common memory leaks:
// 1. Event listeners never removed
// element.addEventListener('click', handler); // Never removeEventListener!
//
// 2. Closures holding references
// function createHandler(bigData) {
// return () => console.log(bigData); // bigData never GC'd!
// }
//
// 3. Intervals/timeouts never cleared
// setInterval(() => checkSomething(), 1000); // Never clearInterval!
//
// 4. DOM references in JS after element removed
// const el = document.getElementById('widget');
// el.remove(); // Element removed from DOM...
// // But el variable still holds reference! Memory not freed.
// Fix patterns:
// Use AbortController for cleanup:
const controller = new AbortController();
element.addEventListener('click', handler, { signal: controller.signal });
// Later: controller.abort(); // Removes all listeners with this signal
// Use WeakMap/WeakRef for caches:
const cache = new WeakMap(); // Keys can be garbage collected!
cache.set(element, heavyData);
// When element is removed from DOM → cache entry auto-cleaned
Step 9: Source Maps
// Your minified production code is unreadable.
// Source maps make it debuggable again.
// webpack.config.js
module.exports = {
devtool: 'source-map', // Generates .map file
// Options:
// 'source-map' → Separate .map file (best for production)
// 'cheap-source-map' → Faster to generate, columns not mapped
// 'eval-source-map' → Each module as eval with source map
// 'hidden-source-map → Generate map but don't reference it (security)
};
// In production: consider 'hidden-source-map'
// You have the maps for debugging but don't expose them publicly
// Upload maps to error tracking service (Sentry, Rollbar) instead
My Debugging Workflow
1. Understand the bug (reproduce it consistently)
2. Read the error message carefully
3. Add strategic console.logs (or use debugger;)
4. Narrow down WHERE the problem is
5. Identify WHAT the actual problem is
6. Form a hypothesis about WHY
7. Test the hypothesis
8. Fix it
9. Verify the fix (not just "it works now")
10. Add a test so it doesn't come back
What's your go-to debugging technique?
Follow @armorbreak for more JavaScript content.
Top comments (0)