Every developer spends a significant portion of their time debugging. The difference between a junior and senior developer often isn't the number of bugs they encounter — it's how quickly and systematically they identify and fix them. JavaScript, with its asynchronous nature, dynamic typing, and browser environment complexity, presents unique debugging challenges. This guide walks you through the full debugging toolkit, from console methods you might not know about to advanced DevTools techniques for async code.
1. Console Methods Beyond console.log()
Most developers reach for console.log() by default, but the Console API has a rich set of methods designed for different debugging scenarios. Using the right method saves time and makes your debug output far more readable.
console.table() — Tabular Data
When debugging arrays of objects, console.table() renders them as a sortable, filterable table in DevTools. This is vastly superior to scrolling through nested object output.
const users = [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'editor' },
{ id: 3, name: 'Charlie', role: 'viewer' },
];
// Display as a sortable table
console.table(users);
// Show only specific columns
console.table(users, ['name', 'role']);
console.trace() — Call Stack Tracing
When you need to know how a function was called, console.trace() prints the full call stack at the point of invocation.
function processOrder(order) {
console.trace('processOrder called');
// You'll see the full chain:
// processOrder < handleCheckout < onSubmit < addEventListener
}
console.group() & console.groupEnd() — Organised Output
Group related log messages together to keep your console output clean, especially when debugging loops or recursive functions.
function debugUser(user) {
console.group(`User: ${user.name}`);
console.log('ID:', user.id);
console.log('Email:', user.email);
console.log('Permissions:', user.permissions);
console.groupEnd();
}
// Use console.groupCollapsed() for collapsed groups by default
users.forEach(user => {
console.groupCollapsed(`User #${user.id}`);
console.log('Details:', user);
console.groupEnd();
});
console.time() & console.timeEnd() — Performance Measurement
Measure how long a block of code takes to execute without external tools.
console.time('dataFetch');
const response = await fetch('/api/users');
const data = await response.json();
console.timeEnd('dataFetch');
// Output: "dataFetch: 142.5ms"
console.assert() — Conditional Logging
Log a message only when a condition is false — perfect for sanity checks that shouldn't clutter the console when things are working.
console.assert(user !== null, 'User should not be null at this point'); console.assert(items.length > 0, 'Cart should not be empty during checkout');
console.count() & console.countReset()
Track how many times a piece of code is executed — useful for detecting unexpected re-renders in frameworks or redundant function calls.
function renderComponent(name) {
console.count(`render:${name}`);
// Output: "render:Header: 1", "render:Header: 2", etc.
}
// Reset when needed
console.countReset('render:Header');
2. Chrome DevTools Deep Dive
Breakpoints
Breakpoints are more powerful than console.log() because they pause execution and let you inspect the entire application state. DevTools supports several types:
- Line-of-code breakpoints — click the line number in the Sources panel. The most common type.
- Conditional breakpoints — right-click a line number and add a condition like
user.id === 42. Execution only pauses when the condition is true. - DOM breakpoints — right-click an element in the Elements panel and select "Break on…" to pause when the DOM node is modified, removed, or has subtree changes.
- XHR/Fetch breakpoints — pause when a network request matches a URL pattern. Found in the Sources panel under "XHR/fetch Breakpoints".
- Event listener breakpoints — pause on specific events like
click,keydown, orscroll.
function processPayment(amount) {
if (amount > 10000) {
debugger; // Execution pauses here when DevTools is open
}
// ... rest of logic
}
The Network Tab
The Network tab is essential for debugging API calls, slow resources, and CORS errors. Key features include:
- Filter by type — isolate XHR/Fetch requests, images, scripts, or stylesheets.
- Inspect request/response headers — critical for debugging authentication, caching, and CORS issues.
- Throttling — simulate slow 3G or offline conditions to test loading states and error handling.
- Copy as cURL — right-click any request to copy it as a cURL command for terminal testing.
- Waterfall view — visualise the loading sequence and identify bottlenecks.
Performance Profiler
The Performance tab records a timeline of everything the browser does — script execution, layout, paint, and compositing. To use it effectively:
- Click the record button or press
Ctrl+E. - Perform the action you want to profile (e.g., scroll, click a button, navigate).
- Stop recording and analyse the flame chart.
- Look for long tasks (blocks exceeding 50ms) — these cause jank and poor INP scores.
- Drill into the call tree to find the specific functions consuming the most time.
3. Source Maps
Modern JavaScript is typically bundled, minified, and sometimes transpiled before reaching the browser. Source maps bridge the gap between your original source code and the transformed output, allowing you to debug against readable code in DevTools.
// webpack.config.js
module.exports = {
// Development: fast rebuilds, accurate line mapping
devtool: 'eval-source-map',
// Production: separate .map files, not shipped to users
// devtool: 'source-map',
};
Best practices for source maps:
- Use
eval-source-maporcheap-module-source-mapin development for fast rebuilds. - Use
source-mapin production — this generates a separate.mapfile. - Configure your server to not serve
.mapfiles publicly if you don't want users to inspect your source. Alternatively, upload maps to your error tracking service (Sentry, Bugsnag). - When you beautify minified JS with Pan Tool's JS Beautify tool, you get readable code for quick inspection even without source maps.
4. Debugging Asynchronous Code
Async bugs are among the hardest to track down because the call stack doesn't tell the full story. Chrome DevTools has improved significantly in this area.
Async Stack Traces
DevTools automatically captures async stack traces for Promises, setTimeout, requestAnimationFrame, and other async APIs. When you hit a breakpoint inside an async function, the call stack panel shows the full chain of async callers.
async function loadUserProfile(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
// Set a breakpoint here to inspect the response
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error('Failed to load user:', error);
throw error; // Re-throw to let the caller handle it
}
}
// Common mistake: forgetting to await
async function init() {
// BUG: missing await — loadUserProfile returns a Promise, not the user
const user = loadUserProfile(42);
console.log(user.name); // undefined!
// FIX:
const user = await loadUserProfile(42);
console.log(user.name); // "Alice"
}
Debugging Promise Rejections
Enable "Pause on caught exceptions" in DevTools Sources panel to catch rejected promises. Also listen for unhandled rejections globally:
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
// Send to your error tracking service
});
5. Common JavaScript Error Types
Understanding error types helps you diagnose problems faster. Here are the most common ones:
- ReferenceError — accessing a variable that hasn't been declared. Common cause: typos, scoping issues, or using a variable before
let/constdeclaration (temporal dead zone). - TypeError — performing an operation on the wrong type, such as calling
undefinedas a function or accessing a property onnull. The most frequent runtime error in JavaScript. - SyntaxError — invalid code structure. Usually caught at parse time, but can appear at runtime with
eval()orJSON.parse(). - RangeError — a value is outside an allowed range. Common with infinite recursion (
Maximum call stack size exceeded) or invalid array lengths. - URIError — incorrect use of URI handling functions like
decodeURIComponent()with malformed input.
6. Robust Try/Catch Patterns
Error handling isn't just about preventing crashes — it's about providing meaningful feedback and maintaining application state.
// Custom error classes for domain-specific errors
class ApiError extends Error {
constructor(message, statusCode, responseBody) {
super(message);
this.name = 'ApiError';
this.statusCode = statusCode;
this.responseBody = responseBody;
}
}
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
// Use specific catch blocks (or check error types)
async function submitForm(formData) {
try {
validate(formData);
const result = await sendToApi(formData);
return result;
} catch (error) {
if (error instanceof ValidationError) {
showFieldError(error.field, error.message);
} else if (error instanceof ApiError) {
if (error.statusCode === 401) {
redirectToLogin();
} else {
showNotification('Server error. Please try again.');
}
} else {
// Unexpected error — log and report
console.error('Unexpected error:', error);
reportToErrorService(error);
showNotification('Something went wrong.');
}
} finally {
// Always runs — clean up loading states
hideLoadingSpinner();
}
}
Error Handling Anti-Patterns to Avoid
- Empty catch blocks —
catch (e) {}silently swallows errors, making bugs invisible. - Catching too broadly — catching all errors at a low level prevents callers from handling them appropriately.
- Not re-throwing — if you can't fully handle an error, log it and re-throw so the caller can respond.
- Using try/catch around synchronous code that can't throw — unnecessary wrapping adds noise.
7. Debugging Tips & Workflow
Adopt these habits to debug faster and more systematically:
- Reproduce first — before you fix anything, find the smallest set of steps that reliably triggers the bug.
- Bisect the problem — use
debuggerstatements or breakpoints to narrow down where the unexpected behaviour begins. Comment out sections of code to isolate the issue. - Read the error message — JavaScript error messages are more informative than developers give them credit for. Read the full stack trace, not just the first line.
- Use the prettifier — if you're debugging production code, use a JS beautifier to make minified code readable before setting breakpoints.
- Check the Network tab first — many "JavaScript bugs" are actually API errors, missing resources, or CORS issues.
- Write a failing test — before fixing the bug, write a test that reproduces it. This ensures the fix works and prevents regressions.
Conclusion
Effective debugging is a skill that compounds over time. By mastering the Console API, leveraging breakpoints and the Performance profiler, understanding source maps, and writing robust error handling patterns, you'll spend less time hunting bugs and more time building features. The next time you encounter a tricky issue, resist the urge to scatter console.log() everywhere — reach for the right tool instead.
Make Minified JavaScript Readable Instantly
Debugging production code? Use Pan Tool's JS Beautify to format minified JavaScript into readable, properly indented source code.