Look, I'll be honest with you. For the first three years of my career as a JavaScript developer, I thought I knew the console API. I mean, how hard could it be, right? You've got console.log(), maybe console.error() when things go wrong, and if you're feeling fancy, console.warn(). That's it. That's the whole toolkit.
Then one day, while pair programming with a senior developer, I watched her debug a gnarly performance issue in about fifteen minutes using console.time() and console.table(). I sat there, mind blown, realizing I'd been debugging like a caveman while a whole arsenal of tools sat unused in my browser.
Here's the thing: the Console API is massive. Most developers use maybe 10% of what's available. The other 90%? It's sitting there, waiting to save you hours of pain, frustration, and those late-night debugging sessions where you're adding console.log('here1'), console.log('here2'), console.log('here3') like some kind of breadcrumb trail of desperation.
I've spent the last decade working on everything from e-commerce platforms handling millions of requests to complex data visualization tools, and I can tell you with absolute certainty: mastering these lesser-known console methods will fundamentally change how you debug. You'll find bugs faster, understand performance bottlenecks better, and honestly, you'll feel like you've unlocked a superpower.
So let's dive into ten console methods that most developers don't know exist, but absolutely should.
1. console.table() - Because Your Arrays and Objects Deserve Better
What It Does
console.table() takes arrays and objects and renders them as an actual, readable table in your console. No more squinting at nested object notation or trying to mentally parse array indices. Just clean, structured data that looks like it belongs in a spreadsheet.
Why Developers Overlook It
Most of us learned console.log() on day one and never looked back. We're creatures of habit. Plus, console.log() works, so why fix what isn't broken? The problem is, console.log() makes complex data structures look like alphabet soup. You're left expanding little arrows, scrolling through nested objects, and losing track of what you're even looking at.
How to Use It
The syntax is dead simple:
console.table(data, [columns]);
The first parameter is your data (array or object). The optional second parameter lets you specify which columns to display.
Real Code Examples
Example 1: Basic Array of Objects
const users = [
{ id: 1, name: 'Sarah Chen', role: 'Developer', active: true },
{ id: 2, name: 'Marcus Thompson', role: 'Designer', active: true },
{ id: 3, name: 'Elena Rodriguez', role: 'Product Manager', active: false },
{ id: 4, name: 'James Wilson', role: 'Developer', active: true }
];
console.table(users);
This renders a beautiful table with columns for index, id, name, role, and active. Each row is perfectly aligned. You can instantly see patterns, spot the inactive user, and understand your data structure at a glance.
Example 2: Filtering Columns
// Only show name and role columns
console.table(users, ['name', 'role']);
This is incredibly useful when you're working with objects that have dozens of properties but you only care about a few.
Example 3: Object of Objects
const apiResponses = {
github: { status: 200, time: 145, cached: false },
twitter: { status: 200, time: 312, cached: true },
stripe: { status: 503, time: 5000, cached: false },
sendgrid: { status: 200, time: 89, cached: true }
};
console.table(apiResponses);
Now you can immediately see that Stripe is down and taking forever, while SendGrid is blazing fast and cached. Try getting that insight from console.log(apiResponses) and you'll be there all day.
Example 4: Nested Data (with caveats)
const complexData = [
{
user: 'Alice',
stats: { posts: 45, likes: 230 },
lastLogin: new Date('2024-11-28')
},
{
user: 'Bob',
stats: { posts: 12, likes: 89 },
lastLogin: new Date('2024-11-30')
}
];
console.table(complexData);
You'll notice that nested objects display as [object Object]. That's a limitation. For nested data, you might want to flatten it first or use console.table() on the nested portion specifically.
Pro Tips
-
Combine with Array Methods: Use
console.table()at the end of a chain to see transformation results:
console.table(
users
.filter(u => u.active)
.map(u => ({ name: u.name, role: u.role }))
);
- Sort Before Display: The browser doesn't sort the table for you, so prepare your data:
console.table(users.sort((a, b) => a.name.localeCompare(b.name)));
-
Use It for API Response Debugging: When you're working with REST APIs that return arrays of data,
console.table()is your best friend. You can immediately spot inconsistencies, missing fields, or unexpected values.
Advanced Use Cases
Performance Comparison Tables
const performanceMetrics = [];
function measureOperation(name, fn) {
const start = performance.now();
fn();
const end = performance.now();
performanceMetrics.push({
operation: name,
duration: `${(end - start).toFixed(2)}ms`
});
}
measureOperation('For Loop', () => {
for (let i = 0; i < 100000; i++) { /* work */ }
});
measureOperation('forEach', () => {
Array.from({ length: 100000 }).forEach(() => { /* work */ });
});
measureOperation('Map', () => {
Array.from({ length: 100000 }).map(() => { /* work */ });
});
console.table(performanceMetrics);
Now you've got a clear performance comparison right there in your console.
Database Query Results
If you're using something like MongoDB or working with SQL query results (converted to JSON), console.table() makes reviewing query results infinitely easier than raw logs.
Mistakes to Avoid
Don't Use It for Massive Arrays: Displaying 10,000 rows in a table will freeze your browser. Filter your data first.
Watch Out for Circular References: If your objects have circular references,
console.table()might not handle them gracefully. Chrome usually manages, but Firefox can choke.Remember It's Read-Only: You can't edit the values in the console table and have them reflect back to your code. It's purely for visualization.
How It Saves Debugging Time
Imagine debugging a function that processes user data. With console.log(), you're looking at something like:
[{id: 1, name: "Sarah", ...}, {id: 2, name: "Marcus", ...}, ...]
You have to click to expand, scroll, compare values mentally. With console.table(), you see everything at once. You immediately spot that user ID 7 has a null email address, or that three users have malformed phone numbers. What would've taken five minutes of clicking and scrolling takes five seconds.
2. console.time() and console.timeEnd() - Your Performance Debugging Swiss Army Knife
What They Do
These methods work as a pair. console.time() starts a timer with a specific label, and console.timeEnd() stops it and logs the elapsed time. It's like having a stopwatch built directly into your code.
Why Developers Overlook Them
Most developers know about the Performance tab in DevTools, which is great for complex profiling. But sometimes you don't need a flame graph—you just need to know if this one function is slow. Reaching for Date.now() or performance.now() and doing manual math feels clunky. console.time() is so simple that people don't realize it exists.
How to Use Them
console.time('labelName');
// ... code you want to measure ...
console.timeEnd('labelName');
The label must match exactly. The browser will log something like: labelName: 234.56ms
Real Code Examples
Example 1: Basic Function Timing
console.time('fetchUserData');
async function fetchUserData() {
const response = await fetch('/api/users');
const data = await response.json();
return data;
}
const users = await fetchUserData();
console.timeEnd('fetchUserData');
// Output: fetchUserData: 347.82ms
Example 2: Comparing Algorithm Performance
const largeArray = Array.from({ length: 100000 }, (_, i) => i);
// Test forEach
console.time('forEach');
largeArray.forEach(num => num * 2);
console.timeEnd('forEach');
// Test map
console.time('map');
largeArray.map(num => num * 2);
console.timeEnd('map');
// Test for loop
console.time('for-loop');
for (let i = 0; i < largeArray.length; i++) {
largeArray[i] * 2;
}
console.timeEnd('for-loop');
// Outputs might be:
// forEach: 8.23ms
// map: 12.45ms
// for-loop: 3.67ms
Now you've got empirical data about which approach is fastest in your specific scenario.
Example 3: Nested Timers
console.time('entire-operation');
console.time('step-1-database');
await database.query('SELECT * FROM users');
console.timeEnd('step-1-database');
console.time('step-2-processing');
const processed = processData(rawData);
console.timeEnd('step-2-processing');
console.time('step-3-rendering');
renderToDOM(processed);
console.timeEnd('step-3-rendering');
console.timeEnd('entire-operation');
// Outputs:
// step-1-database: 234.12ms
// step-2-processing: 45.67ms
// step-3-rendering: 12.34ms
// entire-operation: 292.45ms
This tells you exactly where your bottleneck is. In this case, the database query is eating up 80% of your time.
Example 4: Timing User Interactions
button.addEventListener('click', () => {
console.time('button-click-handler');
// Simulate complex operations
const result = performHeavyCalculation();
updateUI(result);
console.timeEnd('button-click-handler');
});
If this logs button-click-handler: 1247.89ms, you know why users are complaining about sluggish UI.
Pro Tips
Use Descriptive Labels: Don't use generic labels like 'timer1' or 'test'. Use
'api-fetch-user-profile'or'sort-10k-products'. Future you will be grateful.Pair with console.timeLog(): There's also
console.timeLog(label)which logs the current elapsed time without stopping the timer:
console.time('long-operation');
await step1();
console.timeLog('long-operation'); // long-operation: 123ms
await step2();
console.timeLog('long-operation'); // long-operation: 456ms
await step3();
console.timeEnd('long-operation'); // long-operation: 789ms
- Automate Timing with Wrappers: Create a utility function:
async function timeAsync(label, asyncFn) {
console.time(label);
try {
return await asyncFn();
} finally {
console.timeEnd(label);
}
}
const data = await timeAsync('fetch-data', () => fetch('/api/data'));
Advanced Use Cases
A/B Testing Performance
function measureImplementation(name, implementation, iterations = 1000) {
console.time(name);
for (let i = 0; i < iterations; i++) {
implementation();
}
console.timeEnd(name);
}
measureImplementation('string-concat', () => {
let str = '';
for (let i = 0; i < 1000; i++) str += 'x';
});
measureImplementation('array-join', () => {
const arr = [];
for (let i = 0; i < 1000; i++) arr.push('x');
arr.join('');
});
Lazy Loading Performance
console.time('initial-bundle');
// Initial JavaScript execution
console.timeEnd('initial-bundle');
button.addEventListener('click', async () => {
console.time('dynamic-import');
const module = await import('./heavy-feature.js');
console.timeEnd('dynamic-import');
console.time('feature-initialization');
module.initialize();
console.timeEnd('feature-initialization');
});
Mistakes to Avoid
Forgetting to Call timeEnd(): If you forget, the timer just stays open. No error, no warning. Always pair them up.
Label Mismatches:
console.time('myTimer')andconsole.timeEnd('mytimer')won't work. JavaScript is case-sensitive.Don't Use for Production Monitoring: These are development tools. For production performance monitoring, use proper APM tools like New Relic, Datadog, or the Performance API with analytics.
Avoid Timing Asynchronous Code Without Await: This won't work:
console.time('fetch');
fetch('/api/data'); // Don't await
console.timeEnd('fetch'); // Logs ~0ms because fetch hasn't completed
How It Saves Debugging Time
You're getting reports that your app is slow. Where do you start? Instead of guessing or instrumenting complex profiling, you drop console.time() and console.timeEnd() around suspicious code blocks. Within minutes, you've identified that yes, the slow part is the image processing function that's taking 2 seconds per image. Now you know exactly where to optimize.
Without these methods, you'd be using performance.now(), doing manual subtraction, and littering your code with timing logic. console.time() is cleaner, more readable, and faster to implement.
3. console.trace() - Follow the Breadcrumbs Back to the Source
What It Does
console.trace() prints a stack trace to the console, showing you the complete call path that led to that line of code. It's like a "How did I get here?" button for your code.
Why Developers Overlook It
Most developers only see stack traces when errors are thrown. They don't realize you can generate them on demand. When debugging complex applications with deep call stacks, knowing how a function was called is often more important than knowing that it was called.
How to Use It
console.trace('Optional label');
That's it. The browser will output the full stack trace from that point.
Real Code Examples
Example 1: Tracking Function Calls
function calculateTotal(items) {
console.trace('calculateTotal called');
return items.reduce((sum, item) => sum + item.price, 0);
}
function processOrder(order) {
const total = calculateTotal(order.items);
return total;
}
function handleCheckout(userId) {
const order = getOrderForUser(userId);
processOrder(order);
}
handleCheckout(12345);
// Output shows:
// console.trace: calculateTotal called
// calculateTotal @ app.js:2
// processOrder @ app.js:7
// handleCheckout @ app.js:12
// (anonymous) @ app.js:15
Now you can see the exact path: handleCheckout → processOrder → calculateTotal.
Example 2: Debugging Event Handlers
function handleClick(event) {
console.trace('Click handler triggered');
updateUI();
}
document.addEventListener('click', handleClick);
// When clicked, you'll see exactly what triggered the click:
// Maybe it was user interaction, or maybe another script called .click()
Example 3: Tracking React Re-renders
function UserProfile({ userId }) {
console.trace('UserProfile rendering');
const user = useUser(userId);
return <div>{user.name}</div>;
}
If this component is re-rendering unexpectedly, the stack trace will show you which parent component update triggered it.
Example 4: Debugging Recursion
function factorial(n) {
if (n === 1) {
console.trace('Base case reached');
return 1;
}
return n * factorial(n - 1);
}
factorial(5);
// Shows the complete recursive call stack
Pro Tips
- Add Context with Labels: Always include a descriptive message:
console.trace(`User ${userId} reached checkout`);
- Combine with Conditionals: Only trace when something unexpected happens:
function updateCart(item) {
if (!item.price) {
console.trace('Item has no price - this should never happen');
}
// rest of code
}
- Use in Library/Framework Code: When using third-party libraries, trace calls to understand how they're being invoked:
const originalFetch = window.fetch;
window.fetch = function(...args) {
console.trace('Fetch called');
return originalFetch.apply(this, args);
};
Advanced Use Cases
Tracking State Mutations
const state = {
_count: 0,
get count() {
return this._count;
},
set count(value) {
console.trace(`Count changed from ${this._count} to ${value}`);
this._count = value;
}
};
state.count = 5; // Shows who changed it
Finding Memory Leak Sources
class CacheManager {
constructor() {
this.cache = new Map();
}
set(key, value) {
if (this.cache.size > 1000) {
console.trace('Cache exceeded 1000 entries - potential memory leak');
}
this.cache.set(key, value);
}
}
Debugging Third-Party Integration
// Wrap a third-party function to see when it's called
const analytics = window.analytics;
window.analytics.track = function(...args) {
console.trace('Analytics track called with:', args);
return analytics.track.apply(this, args);
};
Mistakes to Avoid
Don't Leave Them in Production: Stack traces have a performance cost. Remove them before shipping.
Be Aware of Browser Differences: The stack trace format varies between Chrome, Firefox, and Safari. Don't rely on parsing the output programmatically.
Async Stack Traces Can Be Misleading: With async/await and promises, the stack trace might not show the full picture. Modern browsers have async stack traces, but they're not always perfect.
Too Many Traces = Noise: If you trace everything, you'll drown in output. Be surgical.
How It Saves Debugging Time
You've got a bug where a value is being set incorrectly, but you don't know where. You could add console.log() statements in 20 different places, or you could add one console.trace() where the incorrect value is being set and immediately see the call path.
I once debugged a Redux action that was being dispatched from an unexpected place. Instead of searching through thousands of lines of code, I added console.trace() in the reducer. The stack trace pointed me directly to a third-party library that was dispatching actions I didn't know about. Saved me hours.
4. console.assert() - The Inline Sanity Check
What It Does
console.assert() evaluates a condition and only logs an error if the condition is false. It's like a mini unit test built into your runtime code.
Why Developers Overlook It
Most developers think of assertions as something for test frameworks like Jest or Mocha. They don't realize the console API has built-in assertion capabilities. It's also not as visible as console.log(), so it doesn't come to mind when debugging.
How to Use It
console.assert(condition, message, ...optionalData);
If condition is truthy, nothing happens. If it's falsy, you get an error message in the console.
Real Code Examples
Example 1: Basic Assertions
function divide(a, b) {
console.assert(b !== 0, 'Divide by zero error', { a, b });
return a / b;
}
divide(10, 2); // No output
divide(10, 0); // Assertion failed: Divide by zero error {a: 10, b: 0}
Example 2: Type Checking
function processUser(user) {
console.assert(typeof user === 'object', 'User must be an object');
console.assert('id' in user, 'User must have an id', user);
console.assert('email' in user, 'User must have an email', user);
// Process user...
}
processUser({ id: 1 }); // Assertion failed: User must have an email
Example 3: Array Validation
function processItems(items) {
console.assert(Array.isArray(items), 'Items must be an array', items);
console.assert(items.length > 0, 'Items array cannot be empty');
console.assert(
items.every(item => item.id),
'All items must have an id',
items.filter(item => !item.id)
);
// Process items...
}
Example 4: State Invariants
class ShoppingCart {
constructor() {
this.items = [];
this.total = 0;
}
addItem(item) {
this.items.push(item);
this.total += item.price;
// Assert invariant: total should equal sum of item prices
const expectedTotal = this.items.reduce((sum, i) => sum + i.price, 0);
console.assert(
this.total === expectedTotal,
'Cart total mismatch',
{ actual: this.total, expected: expectedTotal }
);
}
}
Pro Tips
- Include Context Data: The optional parameters can be any value:
console.assert(
user.age >= 18,
'User must be 18+',
{ user, currentAge: user.age, required: 18 }
);
- Use for Preconditions: Check assumptions at the start of functions:
function updateProfile(userId, updates) {
console.assert(userId, 'userId is required');
console.assert(Object.keys(updates).length > 0, 'updates cannot be empty');
// Function logic...
}
- Check API Responses: Validate data from external sources:
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
console.assert(user.id === id, 'Response ID mismatch', { requested: id, received: user.id });
console.assert(user.email, 'User missing email', user);
return user;
}
Advanced Use Cases
Performance Assertions
function criticalOperation() {
const start = performance.now();
// ... operation ...
const duration = performance.now() - start;
console.assert(
duration < 100,
'Operation took too long',
{ duration: `${duration}ms`, threshold: '100ms' }
);
}
Development-Only Checks
const isDev = process.env.NODE_ENV === 'development';
function transferFunds(from, to, amount) {
if (isDev) {
console.assert(from.balance >= amount, 'Insufficient funds');
console.assert(amount > 0, 'Amount must be positive');
console.assert(from.id !== to.id, 'Cannot transfer to same account');
}
// Transfer logic...
}
React Component Prop Validation
function UserCard({ user, onEdit, theme }) {
console.assert(user, 'UserCard: user prop is required');
console.assert(user.name, 'UserCard: user.name is required', user);
console.assert(
typeof onEdit === 'function',
'UserCard: onEdit must be a function',
{ onEdit }
);
console.assert(
['light', 'dark'].includes(theme),
'UserCard: invalid theme',
{ theme }
);
// Component render...
}
Mistakes to Avoid
-
Don't Use for Error Handling:
console.assert()doesn't throw errors or stop execution. It just logs. For actual error handling, usethrow:
// Wrong - code continues executing
console.assert(user.isAdmin, 'Must be admin');
deleteAllUsers(); // Still runs!
// Right - stops execution
if (!user.isAdmin) throw new Error('Must be admin');
deleteAllUsers(); // Doesn't run
- Assertions Shouldn't Have Side Effects: Don't do this:
console.assert(userId = getUserId(), 'No user ID'); // Assignment in assertion!
-
Be Careful with Truthy/Falsy: Remember that
0,'', andnullare falsy:
console.assert(count, 'Count is required'); // Fails when count is 0!
console.assert(count !== undefined, 'Count is required'); // Better
How It Saves Debugging Time
Assertions act as runtime documentation and early warning systems. Instead of a mysterious bug manifesting three functions deep, the assertion catches the bad data at the entry point and tells you exactly what's wrong.
I use assertions heavily during development. They've caught countless bugs where I passed arguments in the wrong order, forgot to handle edge cases, or made incorrect assumptions about data structures. The assertion fails immediately, right where the problem originates, instead of causing a cryptic error later.
5. console.group() and console.groupEnd() - Organize Your Console Output Like a Pro
What They Do
These methods let you create collapsible groups in your console output. Everything logged between console.group() and console.groupEnd() is indented and can be collapsed/expanded. Think of it like folders in a file system, but for your logs.
Why Developers Overlook Them
When you're just logging a few things, organization doesn't matter. But when you're logging complex operations with multiple steps, nested function calls, or debugging features with lots of moving parts, your console becomes an unreadable mess. Most developers never learn that they can organize it.
How to Use Them
console.group('Label');
// ... logs ...
console.groupEnd();
// Or use console.groupCollapsed() to start collapsed
console.groupCollapsed('Label');
// ... logs ...
console.groupEnd();
Real Code Examples
Example 1: Basic Grouping
console.group('User Login Process');
console.log('Validating credentials...');
console.log('Checking database...');
console.log('Generating session token...');
console.log('Setting cookies...');
console.groupEnd();
console.group('Loading User Data');
console.log('Fetching profile...');
console.log('Fetching preferences...');
console.log('Fetching notifications...');
console.groupEnd();
Now instead of a flat list of 8 logs, you have two organized groups that clearly show different operations.
Example 2: Nested Groups
console.group('Processing Order #12345');
console.group('Validating Items');
console.log('Item 1: Widget - $10.99 ✓');
console.log('Item 2: Gadget - $24.99 ✓');
console.log('Total items: 2');
console.groupEnd();
console.group('Payment Processing');
console.log('Payment method: Credit Card');
console.log('Amount: $35.98');
console.log('Status: Approved');
console.groupEnd();
console.group('Inventory Update');
console.log('Reducing Widget stock: 150 → 149');
console.log('Reducing Gadget stock: 75 → 74');
console.groupEnd();
console.groupEnd();
This creates a hierarchical structure that's incredibly easy to read and understand.
Example 3: Debugging API Calls
async function fetchUserData(userId) {
console.group(`API Call: GET /users/${userId}`);
console.log('Request initiated at:', new Date().toISOString());
try {
const response = await fetch(`/api/users/${userId}`);
console.group('Response Details');
console.log('Status:', response.status);
console.log('Headers:', response.headers);
console.groupEnd();
const data = await response.json();
console.group('Response Data');
console.table(data);
console.groupEnd();
console.log('✓ Request completed successfully');
} catch (error) {
console.error('✗ Request failed:', error);
}
console.groupEnd();
}
Example 4: React Component Lifecycle
class UserProfile extends React.Component {
componentDidMount() {
console.group(`${this.constructor.name} Lifecycle`);
console.log('componentDidMount');
this.loadUserData();
console.groupEnd();
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
console.group(`${this.constructor.name} Update`);
console.log('User changed:', prevProps.userId, '→', this.props.userId);
this.loadUserData();
console.groupEnd();
}
}
loadUserData() {
console.group('Loading User Data');
console.log('User ID:', this.props.userId);
// ... fetch logic ...
console.groupEnd();
}
}
Pro Tips
- Use groupCollapsed for Non-Critical Info: Start groups collapsed by default to keep your console clean:
console.groupCollapsed('Detailed Debug Info');
console.log('Memory usage:', performance.memory.usedJSHeapSize);
console.log('Network status:', navigator.onLine);
console.groupEnd();
- Always Pair group() and groupEnd(): Use try/finally to guarantee cleanup:
console.group('Critical Operation');
try {
riskyOperation();
} finally {
console.groupEnd(); // Always closes even if error is thrown
}
- Add Visual Indicators: Use emojis or symbols to make groups scannable:
console.group('⚠️ Validation Errors');
console.group('✓ Successful Operations');
console.group('🔍 Debug Info');
Advanced Use Cases
Automated Function Tracing
function trace(fn, name) {
return function(...args) {
console.group(`Function: ${name || fn.name}`);
console.log('Arguments:', args);
try {
const result = fn.apply(this, args);
console.log('Return value:', result);
return result;
} catch (error) {
console.error('Error:', error);
throw error;
} finally {
console.groupEnd();
}
};
}
const tracedAdd = trace((a, b) => a + b, 'add');
tracedAdd(5, 3);
// Output:
// Function: add
// Arguments: [5, 3]
// Return value: 8
Redux Action Logging
const actionLogger = store => next => action => {
console.group(`Action: ${action.type}`);
console.log('Payload:', action.payload);
console.log('Previous State:', store.getState());
const result = next(action);
console.log('New State:', store.getState());
console.groupEnd();
return result;
};
Test Suite Organization
function describe(suiteName, tests) {
console.group(`Test Suite: ${suiteName}`);
tests();
console.groupEnd();
}
function it(testName, testFn) {
try {
testFn();
console.log(`✓ ${testName}`);
} catch (error) {
console.error(`✗ ${testName}`, error);
}
}
describe('Calculator', () => {
it('should add numbers', () => {
console.assert(add(2, 3) === 5);
});
it('should multiply numbers', () => {
console.assert(multiply(2, 3) === 6);
});
});
Performance Profiling with Groups
class PerformanceMonitor {
static start(label) {
console.group(`⏱️ ${label}`);
console.time(label);
}
static end(label) {
console.timeEnd(label);
console.groupEnd();
}
static checkpoint(message) {
console.log(` ↳ ${message}`);
}
}
PerformanceMonitor.start('Page Load');
PerformanceMonitor.checkpoint('DOM Ready');
// ... load assets ...
PerformanceMonitor.checkpoint('Assets Loaded');
// ... initialize app ...
PerformanceMonitor.checkpoint('App Initialized');
PerformanceMonitor.end('Page Load');
Mistakes to Avoid
- Forgetting to Close Groups: Unclosed groups will indent everything that comes after:
console.group('Process A');
// ... logs ...
// Forgot console.groupEnd()!
console.log('This will be incorrectly indented');
- Over-Nesting: More than 3-4 levels deep becomes hard to read:
// Too deep!
console.group('Level 1');
console.group('Level 2');
console.group('Level 3');
console.group('Level 4');
console.group('Level 5'); // Nobody wants to expand this many levels
- Grouping Single Items: Don't create a group for one log:
// Pointless
console.group('User');
console.log(user);
console.groupEnd();
// Just do this
console.log('User:', user);
- Not Using groupCollapsed: If you're logging tons of debug info, start collapsed:
// This will clutter your console
console.group('Detailed Request Info');
// ... 50 lines of logs ...
console.groupEnd();
// This keeps it clean
console.groupCollapsed('Detailed Request Info');
// ... 50 lines of logs ...
console.groupEnd();
How It Saves Debugging Time
Imagine debugging a complex checkout flow with payment processing, inventory updates, email notifications, and analytics tracking. Without groups, you've got 100+ log statements all jumbled together. You're scrolling, searching, trying to figure out which logs belong to which operation.
With groups, you see:
- 📦 Order Processing (collapsed)
- 💳 Payment (expanded because that's where the bug is)
- Card validation
- Charge processing ← error here
- Receipt generation
- 📧 Email Notification (collapsed)
- 📊 Analytics (collapsed)
You immediately focus on the Payment group, ignore everything else, and find your bug in seconds instead of minutes.
6. console.dir() - Deep Dive into Object Properties
What It Does
console.dir() displays an interactive listing of an object's properties. Unlike console.log(), which tries to display objects in a "pretty" way (especially for DOM elements), console.dir() always shows the JavaScript object representation with all its properties and methods.
Why Developers Overlook It
console.log() works fine for most objects, so people never explore alternatives. But console.log() has special formatting for certain types (DOM nodes, arrays, etc.) that sometimes hides what you need to see. console.dir() gives you the raw, unfiltered object structure.
How to Use It
console.dir(object, options);
The options parameter (mainly used in Node.js) can include depth, colors, etc. In browsers, it's typically just console.dir(object).
Real Code Examples
Example 1: DOM Elements
const button = document.querySelector('button');
console.log(button);
// Shows: <button class="btn">Click me</button>
// Looks like HTML, shows the element visually
console.dir(button);
// Shows: button {className: "btn", innerHTML: "Click me", onclick: null, ...}
// Shows all properties and methods of the button object
This is huge when you need to see what properties and methods are available on a DOM element.
Example 2: Functions
function greet(name) {
return `Hello, ${name}!`;
}
console.log(greet);
// Shows: ƒ greet(name) { return `Hello, ${name}!`; }
console.dir(greet);
// Shows all function properties:
// {
// length: 1,
// name: "greet",
// arguments: null,
// caller: null,
// prototype: {constructor: ƒ},
// __proto__: ƒ ()
// }
Now you can see the function's length (number of parameters), its prototype, and other metadata.
Example 3: Class Instances
class User {
constructor(name) {
this.name = name;
this.createdAt = new Date();
}
greet() {
return `Hello, ${this.name}`;
}
}
const user = new User('Alice');
console.log(user);
// User {name: "Alice", createdAt: Mon Dec 02 2024...}
console.dir(user);
// Expandable tree showing:
// - name: "Alice"
// - createdAt: Date object
// - __proto__: User
// - greet: ƒ greet()
// - constructor: ƒ User(name)
// - __proto__: Object
This reveals the prototype chain, showing exactly where methods are defined.
Example 4: Arrays with Extra Properties
const arr = [1, 2, 3];
arr.customProp = 'custom value';
arr.customMethod = () => 'custom';
console.log(arr);
// Shows: [1, 2, 3]
// Hides the custom properties!
console.dir(arr);
// Shows:
// Array(3)
// 0: 1
// 1: 2
// 2: 3
// customProp: "custom value"
// customMethod: ƒ ()
// length: 3
// __proto__: Array
Example 5: Inspecting Built-in Objects
console.dir(document);
// Shows all properties and methods of the document object
// Useful for exploring what's available
console.dir(window.localStorage);
// Shows localStorage's prototype chain and methods
console.dir(Promise);
// Shows Promise constructor properties:
// all, allSettled, any, race, reject, resolve, etc.
Pro Tips
- Use for API Exploration: When working with unfamiliar libraries:
import * as THREE from 'three';
console.dir(THREE);
// See all available exports and their structure
- Compare log() vs dir(): When confused about an object's structure:
console.log('log:', myObject);
console.dir(myObject);
// Compare the outputs to understand what's happening
- Inspect Event Objects: Events have tons of properties:
button.addEventListener('click', (event) => {
console.dir(event);
// See all event properties: target, currentTarget, clientX, clientY, etc.
});
- Node.js Options: In Node.js, you can control depth:
console.dir(deeplyNestedObject, { depth: null }); // Show everything
console.dir(deeplyNestedObject, { depth: 2 }); // Limit depth
Advanced Use Cases
Debugging Proxies and Wrapped Objects
const handler = {
get(target, prop) {
console.log(`Getting ${prop}`);
return target[prop];
}
};
const proxy = new Proxy({ name: 'Alice' }, handler);
console.log(proxy); // Might trigger proxy traps
console.dir(proxy); // Shows proxy structure without triggering traps
Inspecting Custom Iterators
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
return {
current: this.from,
last: this.to,
next() {
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
console.dir(range);
// Shows the Symbol.iterator method and all properties
Understanding Inheritance
class Animal {
eat() { return 'eating'; }
}
class Dog extends Animal {
bark() { return 'woof'; }
}
const dog = new Dog();
console.dir(dog);
// Expandable view shows:
// Dog {}
// __proto__: Animal
// bark: ƒ bark()
// constructor: class Dog
// __proto__: Object
// eat: ƒ eat()
// constructor: class Animal
Mistakes to Avoid
- Don't Use for Simple Values: For primitives, it's overkill:
console.dir(42); // Just shows: 42
console.dir("hello"); // Just shows: "hello"
console.log(42); // Same output, more semantic
-
Remember It's Read-Only: You can't edit values in the
console.dir()output (just like regular console):
console.dir(user); // Can't click and edit user.name
- Don't Expect Identical Output Across Browsers: Chrome, Firefox, and Safari display objects differently.
How It Saves Debugging Time
You're using a third-party library and need to know what methods are available. You could read the documentation (if it exists and is up-to-date), or you could do:
import { SomeClass } from 'mystery-library';
const instance = new SomeClass();
console.dir(instance);
Boom. You see every property and method, the prototype chain, inherited properties—everything. You discover that instance.reset() exists, which isn't documented. Problem solved.
Or you're debugging a DOM issue and console.log(element) shows the pretty HTML, but you need to know its offsetWidth, scrollTop, or event handlers. console.dir(element) reveals all of it.
7. console.count() and console.countReset() - Track Function Calls Without the Boilerplate
What They Do
console.count() logs the number of times it's been called with a particular label. console.countReset() resets the counter. It's like having a built-in click counter for your code.
Why Developers Overlook Them
Most developers manually track counts with variables:
let callCount = 0;
function myFunction() {
callCount++;
console.log('Called', callCount, 'times');
}
It works, but it's verbose and you need to manage state. console.count() does this in one line with no extra variables.
How to Use Them
console.count(label); // Increments and logs count
console.countReset(label); // Resets counter to 0
If you don't provide a label, it uses 'default'.
Real Code Examples
Example 1: Basic Counting
function handleClick() {
console.count('button clicks');
// Do click handling...
}
button.addEventListener('click', handleClick);
// First click: button clicks: 1
// Second click: button clicks: 2
// Third click: button clicks: 3
Example 2: Tracking Function Calls
function fetchData(endpoint) {
console.count(`API call to ${endpoint}`);
return fetch(endpoint);
}
fetchData('/users'); // API call to /users: 1
fetchData('/posts'); // API call to /posts: 1
fetchData('/users'); // API call to /users: 2
fetchData('/users'); // API call to /users: 3
This immediately shows you which endpoints are being hit most frequently.
Example 3: Debugging Infinite Loops or Recursion
function problematicRecursion(n) {
console.count('recursion depth');
if (n <= 0) return;
// Oops, forgot to decrement!
problematicRecursion(n);
}
problematicRecursion(5);
// Output:
// recursion depth: 1
// recursion depth: 2
// recursion depth: 3
// ... keeps going ...
You'd instantly see the counter climbing and know you have a recursion problem.
Example 4: React Render Counting
function UserProfile({ userId }) {
console.count(`UserProfile render for user ${userId}`);
// Component logic...
return <div>Profile</div>;
}
// Output shows exactly how many times and for which users:
// UserProfile render for user 123: 1
// UserProfile render for user 123: 2
// UserProfile render for user 456: 1
Example 5: Event Handler Tracking
input.addEventListener('input', () => {
console.count('input event');
});
input.addEventListener('change', () => {
console.count('change event');
});
// Type "hello":
// input event: 1
// input event: 2
// input event: 3
// input event: 4
// input event: 5
// change event: 1 (when you blur)
Pro Tips
- Use Descriptive Labels: Make them meaningful:
// Bad
console.count('x');
// Good
console.count('validation-failure');
console.count('cache-hit');
console.count('database-query');
- Reset When Needed: Clear counts between test runs:
function runTest() {
console.countReset('test-assertion');
// Run test...
}
function assert(condition) {
if (!condition) {
console.count('test-assertion');
}
}
- Combine with Conditionals: Only count certain scenarios:
function processData(data) {
if (data.size > 10000) {
console.count('large-dataset');
}
if (data.hasErrors) {
console.count('data-errors');
}
}
- Track Multiple Metrics Simultaneously:
function handleRequest(req) {
console.count('total-requests');
if (req.method === 'GET') {
console.count('get-requests');
} else if (req.method === 'POST') {
console.count('post-requests');
}
if (req.authenticated) {
console.count('authenticated-requests');
} else {
console.count('anonymous-requests');
}
}
Advanced Use Cases
Rate Limiting Detection
let requestCount = 0;
async function apiCall() {
console.count('API requests this session');
requestCount++;
if (requestCount > 100) {
console.warn('Approaching rate limit!');
}
// Make request...
}
Memory Leak Detection
class ResourceManager {
constructor() {
console.count('ResourceManager instances created');
}
destroy() {
console.count('ResourceManager instances destroyed');
}
}
// If created count far exceeds destroyed count, you have a leak
A/B Test Tracking
function showFeature(variant) {
console.count(`feature-variant-${variant}`);
if (variant === 'A') {
showVariantA();
} else {
showVariantB();
}
}
// Quick visual check of variant distribution:
// feature-variant-A: 523
// feature-variant-B: 477
Debugging Polling
let pollCount = 0;
const MAX_POLLS = 10;
async function pollForResult() {
console.count('poll-attempt');
pollCount++;
const result = await checkStatus();
if (result.complete) {
console.log(`✓ Completed after ${pollCount} polls`);
console.countReset('poll-attempt');
return result;
}
if (pollCount < MAX_POLLS) {
setTimeout(pollForResult, 1000);
} else {
console.error('Max polls reached');
}
}
Mistakes to Avoid
- Don't Use for Production Metrics: These are development tools. For production, use proper analytics:
// Development: OK
console.count('user-signup');
// Production: Use analytics service
analytics.track('user-signup');
- Remember Labels Are Case-Sensitive:
console.count('MyLabel');
console.count('mylabel');
// These are DIFFERENT counters!
- Counts Persist: Counters don't automatically reset between function calls:
function processItems(items) {
items.forEach(item => {
console.count('item-processed');
});
// Counter keeps incrementing across multiple processItems calls
}
// If you want to reset:
function processItems(items) {
console.countReset('item-processed');
items.forEach(item => {
console.count('item-processed');
});
}
How It Saves Debugging Time
You suspect a function is being called more often than it should. Instead of adding a counter variable, initializing it, incrementing it, and logging it, you drop in one line:
console.count('suspiciousFunction');
Immediately, you see it's being called 50 times when it should be called once. Bug found in 10 seconds.
Or you're debugging event listeners and can't figure out if both listeners are firing. Add console.count() to each and watch the output. If you only see counts for one, you know the other isn't attached properly.
8. console.clear() - Spring Cleaning for Your Console
What It Does
console.clear() clears all console output. It's like hitting the "clear" button in DevTools, but from your code.
Why Developers Overlook It
Most developers manually clear the console using the browser's clear button or DevTools keyboard shortcuts. They don't realize they can do it programmatically, which enables some powerful debugging patterns.
How to Use It
console.clear();
That's it. No parameters, no complexity.
Real Code Examples
Example 1: Clear Before Major Operations
function runDiagnostics() {
console.clear();
console.log('=== Running System Diagnostics ===');
checkDatabase();
checkAPI();
checkCache();
console.log('=== Diagnostics Complete ===');
}
Now each diagnostic run starts with a clean slate, making it easy to see just the current run's output.
Example 2: Clear on Page Navigation
// In a Single Page Application
router.on('navigate', (route) => {
if (process.env.NODE_ENV === 'development') {
console.clear();
console.log(`Navigated to: ${route.path}`);
}
});
Each route gets fresh console output, preventing confusion between different pages.
Example 3: Game Loop Debugging
function gameLoop() {
// Clear console each frame for real-time debugging
console.clear();
console.log('=== Frame', frameCount, '===');
console.log('Player Position:', player.x, player.y);
console.log('Enemy Count:', enemies.length);
console.log('Score:', score);
console.log('FPS:', calculateFPS());
requestAnimationFrame(gameLoop);
}
You get a live, updating display of game state without console clutter.
Example 4: Testing Suite Reset
function runTestSuite(tests) {
console.clear();
console.log('╔════════════════════════════╗');
console.log('║ Test Suite Starting ║');
console.log('╚════════════════════════════╝');
tests.forEach(test => {
try {
test();
console.log(`✓ ${test.name}`);
} catch (error) {
console.error(`✗ ${test.name}:`, error);
}
});
}
Example 5: Development Mode Console Management
class DevConsole {
static enabled = process.env.NODE_ENV === 'development';
static section(title) {
if (!this.enabled) return;
console.clear();
console.log('═'.repeat(50));
console.log(title.toUpperCase().padStart(25 + title.length / 2));
console.log('═'.repeat(50));
}
}
// Usage
DevConsole.section('User Authentication');
console.log('Checking credentials...');
// ... auth logic ...
DevConsole.section('Loading Dashboard');
console.log('Fetching user data...');
// ... dashboard logic ...
Pro Tips
- Conditional Clearing: Only clear in development:
if (process.env.NODE_ENV === 'development') {
console.clear();
}
- Preserve Important Logs: Log critical info after clearing:
console.clear();
console.log('App Version:', APP_VERSION);
console.log('Environment:', process.env.NODE_ENV);
console.log('User ID:', currentUser.id);
console.log('─'.repeat(50));
// Now start your actual logging
- Create Clear Points in Long Operations:
async function longProcess() {
console.clear();
console.log('Phase 1: Data Collection');
await collectData();
console.clear();
console.log('Phase 2: Processing');
await processData();
console.clear();
console.log('Phase 3: Output');
await generateOutput();
}
- Keyboard Shortcut Replacement: Bind to a key for quick clearing:
window.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'l') {
e.preventDefault();
console.clear();
console.log('Console cleared manually');
}
});
Advanced Use Cases
Live Data Monitor
class Monitor {
constructor(interval = 1000) {
this.metrics = {};
this.interval = interval;
}
start() {
this.timer = setInterval(() => {
console.clear();
console.log('═══ LIVE METRICS ═══');
console.log('Updated:', new Date().toLocaleTimeString());
console.log('');
console.table(this.metrics);
}, this.interval);
}
update(key, value) {
this.metrics[key] = value;
}
stop() {
clearInterval(this.timer);
}
}
const monitor = new Monitor();
monitor.start();
// Somewhere in your app:
monitor.update('Active Users', getActiveUsers());
monitor.update('Memory Usage', getMemoryUsage());
monitor.update('API Latency', getAPILatency());
Interactive Debug Menu
const debugMenu = {
showState() {
console.clear();
console.log('📊 APPLICATION STATE');
console.log('User:', currentUser);
console.log('Route:', currentRoute);
console.log('Store:', store.getState());
},
showPerformance() {
console.clear();
console.log('⚡ PERFORMANCE METRICS');
console.table(performance.getEntries());
},
showNetwork() {
console.clear();
console.log('🌐 NETWORK REQUESTS');
// Log network stats
}
};
// Access from browser console:
// debugMenu.showState()
// debugMenu.showPerformance()
Animated Console Output
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let frameIndex = 0;
function showLoader(message) {
const interval = setInterval(() => {
console.clear();
console.log(`${frames[frameIndex]} ${message}`);
frameIndex = (frameIndex + 1) % frames.length;
}, 80);
return () => {
clearInterval(interval);
console.clear();
};
}
// Usage
const stopLoader = showLoader('Loading data...');
await fetchData();
stopLoader();
console.log('✓ Data loaded');
Mistakes to Avoid
- Don't Clear in Production: Remove console.clear() from production builds:
// This will annoy users debugging your site
setInterval(() => console.clear(), 1000); // DON'T DO THIS
- Don't Clear Away Errors: Be careful not to clear important error messages:
try {
riskyOperation();
} catch (error) {
console.error(error); // Log the error first
console.clear(); // Now it's gone! Bad!
}
- Excessive Clearing = Unreadable Console: Don't clear too frequently:
// Bad: clears every iteration
for (let i = 0; i < 100; i++) {
console.clear();
console.log(i); // Too fast to read!
}
- Browser Limitations: Some browsers show "Console was cleared" messages. Users might find this annoying.
How It Saves Debugging Time
When debugging complex workflows with multiple steps, you don't want to scroll through hundreds of log lines from previous runs. console.clear() at the start of your debugging session gives you a clean slate.
Or imagine debugging a real-time feature like a chat app or live feed. Old messages clutter your console. Adding console.clear() before logging new messages keeps everything readable.
I use this constantly when debugging animation loops or game logic where I need to see current state without the noise of every previous frame's logs.
9. console.warn() and console.error() - Semantic Logging That Actually Matters
What They Do
console.warn() logs warnings (yellow in most browsers) and console.error() logs errors (red). They work like console.log() but with different visual styles and semantic meaning.
Why Developers Overlook Them
Many developers use console.log() for everything. They think styling doesn't matter, or they don't realize browsers treat these methods differently. But using the right method improves readability, enables filtering, and provides better stack traces.
How to Use Them
console.warn(message, ...optionalData);
console.error(message, ...optionalData);
Both accept multiple arguments just like console.log().
Real Code Examples
Example 1: Deprecation Warnings
function oldAPIMethod(data) {
console.warn(
'oldAPIMethod() is deprecated and will be removed in v3.0. Use newAPIMethod() instead.',
'Called with:', data
);
// Still execute for backward compatibility
return legacyLogic(data);
}
The yellow warning catches attention without stopping execution.
Example 2: Configuration Issues
function initializeApp(config) {
if (!config.apiKey) {
console.error('CRITICAL: API key is missing. App will not function properly.');
throw new Error('Missing API key');
}
if (!config.analyticsId) {
console.warn('WARNING: Analytics ID not provided. Analytics will be disabled.');
}
if (config.debugMode) {
console.warn('App running in debug mode. Performance may be degraded.');
}
// Initialize...
}
Errors are red and critical. Warnings are yellow and informational.
Example 3: Validation Errors
function validateUser(user) {
const errors = [];
const warnings = [];
if (!user.email) {
errors.push('Email is required');
} else if (!isValidEmail(user.email)) {
errors.push('Email format is invalid');
}
if (!user.phone) {
warnings.push('Phone number is recommended but optional');
}
if (user.age < 13) {
errors.push('User must be 13 or older');
} else if (user.age < 18) {
warnings.push('User is under 18 - some features restricted');
}
if (errors.length > 0) {
console.error('User validation failed:', errors);
return false;
}
if (warnings.length > 0) {
console.warn('User validation warnings:', warnings);
}
return true;
}
Example 4: Performance Warnings
function processLargeDataset(data) {
if (data.length > 10000) {
console.warn(
`Processing ${data.length} items may impact performance.`,
'Consider using pagination or virtual scrolling.'
);
}
const start = performance.now();
const result = process(data);
const duration = performance.now() - start;
if (duration > 1000) {
console.error(
`SLOW OPERATION: Processing took ${duration}ms`,
'This will cause UI jank'
);
}
return result;
}
Example 5: API Error Handling
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
if (response.status === 404) {
console.error(`User ${userId} not found`);
} else if (response.status >= 500) {
console.error(`Server error (${response.status}) when fetching user`);
} else {
console.warn(`Unexpected response: ${response.status}`);
}
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to fetch user data:', error);
throw error;
}
}
Pro Tips
-
Stack Traces:
console.error()automatically includes a stack trace in most browsers:
function deepFunction() {
console.error('Something went wrong here');
// Browser shows full call stack automatically
}
- Filter by Type: DevTools lets you filter console output by type. Using the right method makes filtering effective:
// In DevTools, click "Errors" to see only these
console.error('Critical issue');
console.error('Database connection failed');
// Click "Warnings" to see only these
console.warn('Deprecated method used');
console.warn('Low memory warning');
- Add Context Objects: Include relevant data for debugging:
console.error('Failed to save user', {
userId: user.id,
attemptedData: data,
timestamp: new Date(),
sessionId: getSessionId()
});
- Use with Error Objects: Pass actual Error objects for better stack traces:
try {
riskyOperation();
} catch (error) {
console.error('Operation failed:', error);
// Shows error message AND stack trace
}
Advanced Use Cases
Custom Error Levels
const Logger = {
ERROR: 'error',
WARN: 'warn',
INFO: 'info',
DEBUG: 'debug',
log(level, message, data) {
const timestamp = new Date().toISOString();
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
switch(level) {
case this.ERROR:
console.error(prefix, message, data);
// Send to error tracking service
sendToErrorTracking(message, data);
break;
case this.WARN:
console.warn(prefix, message, data);
break;
default:
console.log(prefix, message, data);
}
},
error(message, data) { this.log(this.ERROR, message, data); },
warn(message, data) { this.log(this.WARN, message, data); },
info(message, data) { this.log(this.INFO, message, data); },
debug(message, data) { this.log(this.DEBUG, message, data); }
};
// Usage
Logger.error('Payment processing failed', { orderId: 123, amount: 99.99 });
Logger.warn('API rate limit approaching', { remaining: 10, limit: 100 });
Error Boundary Logging (React)
class ErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
console.error('React Error Boundary caught an error:', {
error: error.toString(),
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString()
});
// Could also send to error tracking service
}
render() {
if (this.state.hasError) {
return <ErrorFallback />;
}
return this.props.children;
}
}
Progressive Error Escalation
class APIClient {
constructor() {
this.retryCount = 0;
this.maxRetries = 3;
}
async request(url) {
try {
const response = await fetch(url);
if (!response.ok) {
this.retryCount++;
if (this.retryCount === 1) {
console.warn(`Request failed (attempt ${this.retryCount}), retrying...`);
} else if (this.retryCount < this.maxRetries) {
console.warn(`Request failed (attempt ${this.retryCount}/${this.maxRetries})`);
} else {
console.error(`Request failed after ${this.maxRetries} attempts`, {
url,
status: response.status
});
throw new Error('Max retries exceeded');
}
await this.delay(1000 * this.retryCount);
return this.request(url);
}
this.retryCount = 0; // Reset on success
return response;
} catch (error) {
console.error('Network error:', error);
throw error;
}
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
Environment-Aware Logging
const isDev = process.env.NODE_ENV === 'development';
const isTest = process.env.NODE_ENV === 'test';
const logger = {
error(...args) {
console.error(...args);
if (!isDev && !isTest) {
sendToErrorTracking(...args);
}
},
warn(...args) {
if (isDev) {
console.warn(...args);
}
},
info(...args) {
if (isDev) {
console.log(...args);
}
}
};
// Now warnings and info only appear in development
logger.warn('This API is slow'); // Dev only
logger.error('Critical failure'); // Always logged + tracked in production
Smart Error Categorization
class ErrorHandler {
static handle(error, context = {}) {
// Network errors
if (error instanceof TypeError && error.message.includes('fetch')) {
console.error('Network Error:', {
message: 'Could not connect to server',
context,
originalError: error
});
return;
}
// Validation errors
if (error.name === 'ValidationError') {
console.warn('Validation Error:', {
message: error.message,
context
});
return;
}
// Permission errors
if (error.status === 403) {
console.error('Permission Denied:', {
message: 'User lacks required permissions',
context
});
return;
}
// Unknown errors
console.error('Unexpected Error:', {
error,
context,
stack: error.stack
});
}
}
// Usage
try {
await saveData(data);
} catch (error) {
ErrorHandler.handle(error, { userId: currentUser.id, action: 'save' });
}
Mistakes to Avoid
- Don't Use error() for Non-Errors:
// Bad - this isn't an error
console.error('User clicked button');
// Good
console.log('User clicked button');
- Don't Overuse warn():
// Too many warnings = noise
console.warn('Processing...');
console.warn('Step 1 complete');
console.warn('Step 2 complete');
// Just use log() for normal flow
console.log('Processing...');
- Don't Swallow Errors Silently:
// Bad - error is caught but not logged
try {
await criticalOperation();
} catch (error) {
// Nothing - error disappears!
}
// Good
try {
await criticalOperation();
} catch (error) {
console.error('Critical operation failed:', error);
throw error; // Or handle appropriately
}
- Don't Log Sensitive Data:
// Dangerous - logs user password
console.error('Login failed', { username, password });
// Safe
console.error('Login failed', { username });
How It Saves Debugging Time
Visual distinction is powerful. When your console has 100 log statements, the bright red errors jump out immediately. You don't waste time reading through normal logs to find problems.
Filtering is even more powerful. Click "Errors only" in DevTools and instantly see just the problems. Click "Warnings" to see potential issues without critical failures.
Plus, error tracking services like Sentry automatically capture console.error() calls in production. Using console.error() correctly means your production errors are automatically logged to your monitoring system without extra code.
10. console.memory - Monitor Memory Usage in Real-Time
What It Does
console.memory (in Chrome-based browsers) provides real-time information about JavaScript heap memory usage. It's not a method—it's a property that returns an object with memory statistics.
Why Developers Overlook It
It's Chrome-specific and not standardized, so it's less well-known. Also, most developers don't think about memory until they have a memory leak. But monitoring memory proactively helps you catch leaks before they become problems.
How to Use It
console.log(console.memory);
// Returns something like:
// {
// jsHeapSizeLimit: 2190000000, // Max heap size
// totalJSHeapSize: 50000000, // Total allocated
// usedJSHeapSize: 30000000 // Actually used
// }
All values are in bytes.
Real Code Examples
Example 1: Basic Memory Check
function checkMemory() {
const memory = console.memory;
const used = (memory.usedJSHeapSize / 1048576).toFixed(2);
const limit = (memory.jsHeapSizeLimit / 1048576).toFixed(2);
const percentage = ((memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100).toFixed(2);
console.log(`Memory: ${used} MB / ${limit} MB (${percentage}%)`);
}
checkMemory();
// Output: Memory: 45.23 MB / 2089.00 MB (2.17%)
Example 2: Monitoring Memory Over Time
function startMemoryMonitor(interval = 5000) {
const readings = [];
const timer = setInterval(() => {
const memory = console.memory;
const usedMB = (memory.usedJSHeapSize / 1048576).toFixed(2);
readings.push({
timestamp: new Date().toLocaleTimeString(),
usedMB: parseFloat(usedMB)
});
console.clear();
console.log('=== MEMORY MONITOR ===');
console.table(readings.slice(-10)); // Show last 10 readings
// Alert if memory is growing too fast
if (readings.length > 2) {
const recent = readings.slice(-3);
const trend = recent[2].usedMB - recent[0].usedMB;
if (trend > 10) {
console.warn(`⚠️ Memory increased by ${trend.toFixed(2)} MB in last 3 readings`);
}
}
}, interval);
return () => clearInterval(timer);
}
const stopMonitor = startMemoryMonitor(2000);
// Run your app, watch for memory leaks
// stopMonitor() when done
Example 3: Before/After Memory Comparison
async function profileMemoryUsage(operationName, operation) {
// Force garbage collection if available (Chrome with --expose-gc flag)
if (global.gc) {
global.gc();
}
const before = console.memory.usedJSHeapSize;
await operation();
const after = console.memory.usedJSHeapSize;
const delta = ((after - before) / 1048576).toFixed(2);
console.log(`${operationName} memory impact: ${delta} MB`);
if (delta > 10) {
console.warn(`High memory usage detected for ${operationName}`);
}
return delta;
}
// Usage
await profileMemoryUsage('Loading 1000 users', async () => {
const users = await fetchUsers(1000);
renderUsers(users);
});
// Output: Loading 1000 users memory impact: 15.23 MB
Example 4: Memory Leak Detector
class MemoryLeakDetector {
constructor(threshold = 50) {
this.baseline = null;
this.threshold = threshold; // MB
this.checkpoints = [];
}
setBaseline() {
this.baseline = console.memory.usedJSHeapSize;
console.log('Memory baseline set:', (this.baseline / 1048576).toFixed(2), 'MB');
}
checkpoint(label) {
const current = console.memory.usedJSHeapSize;
const increase = (current - this.baseline) / 1048576;
this.checkpoints.push({
label,
memory: (current / 1048576).toFixed(2),
increase: increase.toFixed(2)
});
if (increase > this.threshold) {
console.error(`🚨 MEMORY LEAK DETECTED at "${label}"`, {
current: (current / 1048576).toFixed(2) + ' MB',
baseline: (this.baseline / 1048576).toFixed(2) + ' MB',
increase: increase.toFixed(2) + ' MB',
threshold: this.threshold + ' MB'
});
}
return increase;
}
report() {
console.table(this.checkpoints);
}
}
// Usage
const detector = new MemoryLeakDetector(30);
detector.setBaseline();
// Test various operations
loadInitialData();
detector.checkpoint('Initial data loaded');
for (let i = 0; i < 10; i++) {
processLargeDataset();
detector.checkpoint(`Iteration ${i + 1}`);
}
detector.report();
Example 5: Component Memory Profiling
class ComponentProfiler {
static profiles = new Map();
static start(componentName) {
this.profiles.set(componentName, {
startMemory: console.memory.usedJSHeapSize,
startTime: performance.now()
});
}
static end(componentName) {
const profile = this.profiles.get(componentName);
if (!profile) {
console.warn(`No profile started for ${componentName}`);
return;
}
const endMemory = console.memory.usedJSHeapSize;
const endTime = performance.now();
const memoryDelta = ((endMemory - profile.startMemory) / 1048576).toFixed(2);
const timeDelta = (endTime - profile.startTime).toFixed(2);
console.group(`📊 ${componentName} Profile`);
console.log(`Memory: ${memoryDelta} MB`);
console.log(`Time: ${timeDelta} ms`);
if (parseFloat(memoryDelta) > 5) {
console.warn('High memory allocation detected');
}
console.groupEnd();
this.profiles.delete(componentName);
}
}
// Usage in React
function HeavyComponent() {
useEffect(() => {
ComponentProfiler.start('HeavyComponent');
// Component logic...
return () => {
ComponentProfiler.end('HeavyComponent');
};
}, []);
return <div>Heavy Component</div>;
}
Pro Tips
- Convert to Human-Readable Units:
function formatBytes(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1048576) return (bytes / 1024).toFixed(2) + ' KB';
if (bytes < 1073741824) return (bytes / 1048576).toFixed(2) + ' MB';
return (bytes / 1073741824).toFixed(2) + ' GB';
}
console.log('Memory used:', formatBytes(console.memory.usedJSHeapSize));
- Check Browser Support:
if (console.memory) {
console.log('Memory monitoring available');
} else {
console.log('Memory monitoring not supported in this browser');
}
- Combine with Performance API:
function fullSystemCheck() {
console.group('System Status');
if (console.memory) {
console.log('Memory:', formatBytes(console.memory.usedJSHeapSize));
}
if (performance.memory) {
console.log('Performance memory:', formatBytes(performance.memory.usedJSHeapSize));
}
console.log('Navigation timing:', performance.timing);
console.groupEnd();
}
- Watch for Growing Trends:
let lastMemory = console.memory.usedJSHeapSize;
setInterval(() => {
const current = console.memory.usedJSHeapSize;
const delta = current - lastMemory;
if (delta > 1048576) { // 1 MB increase
console.warn('Memory increased by', formatBytes(delta));
}
lastMemory = current;
}, 10000);
Advanced Use Cases
Automatic Memory Snapshots
class MemorySnapshot {
static snapshots = [];
static take(label) {
const snapshot = {
label,
timestamp: Date.now(),
memory: { ...console.memory }
};
this.snapshots.push(snapshot);
console.log(`📸 Snapshot "${label}" taken`);
}
static compare(label1, label2) {
const snap1 = this.snapshots.find(s => s.label === label1);
const snap2 = this.snapshots.find(s => s.label === label2);
if (!snap1 || !snap2) {
console.error('Snapshots not found');
return;
}
const delta = snap2.memory.usedJSHeapSize - snap1.memory.usedJSHeapSize;
const timeDelta = snap2.timestamp - snap1.timestamp;
console.group(`Comparison: ${label1} → ${label2}`);
console.log('Memory change:', formatBytes(delta));
console.log('Time elapsed:', (timeDelta / 1000).toFixed(2), 'seconds');
console.log('Rate:', formatBytes(delta / (timeDelta / 1000)), '/second');
console.groupEnd();
}
static list() {
console.table(this.snapshots.map(s => ({
label: s.label,
time: new Date(s.timestamp).toLocaleTimeString(),
memory: formatBytes(s.memory.usedJSHeapSize)
})));
}
}
// Usage
MemorySnapshot.take('app-start');
// ... run app ...
MemorySnapshot.take('after-initial-load');
// ... user interaction ...
MemorySnapshot.take('after-user-action');
MemorySnapshot.compare('app-start', 'after-initial-load');
MemorySnapshot.list();
Memory Pressure Detection
class MemoryPressureMonitor {
constructor() {
this.threshold = 0.85; // 85% of heap limit
this.listeners = [];
}
start() {
this.interval = setInterval(() => {
const memory = console.memory;
const usage = memory.usedJSHeapSize / memory.jsHeapSizeLimit;
if (usage > this.threshold) {
const event = {
type: 'memory-pressure',
usage: (usage * 100).toFixed(2) + '%',
used: formatBytes(memory.usedJSHeapSize),
limit: formatBytes(memory.jsHeapSizeLimit)
};
console.error('🔴 MEMORY PRESSURE', event);
this.listeners.forEach(listener => listener(event));
}
}, 1000);
}
stop() {
clearInterval(this.interval);
}
onPressure(callback) {
this.listeners.push(callback);
}
}
const monitor = new MemoryPressureMonitor();
monitor.onPressure(event => {
// Take action: clear caches, reduce quality, etc.
console.warn('Reducing app memory footprint...');
clearCaches();
});
monitor.start();
Mistakes to Avoid
- Don't Rely on It in Production: It's Chrome-specific and not standardized:
// Bad - will break in Firefox
const memory = console.memory.usedJSHeapSize;
// Good
const memory = console.memory ? console.memory.usedJSHeapSize : null;
- Garbage Collection Timing: Memory readings can be misleading if GC hasn't run:
// Memory might still be high even after deleting
let bigArray = new Array(1000000);
bigArray = null; // Eligible for GC, but not collected yet
console.log(console.memory.usedJSHeapSize); // Might still show high
- Don't Over-Monitor: Checking memory too frequently can impact performance:
// Bad - checks every 100ms
setInterval(checkMemory, 100);
// Good - checks every 5 seconds
setInterval(checkMemory, 5000);
- Remember It's Heap Only: It doesn't show DOM memory, images, or other resources:
// console.memory won't show this image's memory
const img = new Image();
img.src = 'huge-image.png';
How It Saves Debugging Time
Memory leaks are notoriously hard to track down. Without memory monitoring, you might not even know you have a leak until users complain about browser tabs crashing.
With console.memory, you can actively monitor memory usage during development. Run through a typical user flow, check the memory readings. If memory keeps climbing, you know there's a leak. Add checkpoints at different stages to narrow down where the leak occurs.
I once debugged a memory leak in a data visualization app where memory would grow from 50MB to 500MB after an hour of use. By adding memory checkpoints after each chart render, I discovered that event listeners weren't being cleaned up. Fixed it in 20 minutes thanks to memory monitoring.
Bonus Insights: How the Console Actually Works
Understanding how the console works under the hood makes you a better debugger. Here are some things most developers don't know:
Console is Asynchronous
The console doesn't block your code. When you log something, it's queued and displayed asynchronously. This means:
const obj = { value: 1 };
console.log(obj);
obj.value = 2;
// In the console, you might see { value: 2 }
// even though it was 1 when logged!
This happens because the console often logs a reference, not a snapshot. To avoid this:
// Create a snapshot
console.log(JSON.parse(JSON.stringify(obj)));
// Or use a breakpoint/debugger statement
console.log(obj);
debugger; // Pauses execution so you see the real state
Console Methods Can Be Overridden
You can wrap or replace console methods:
const originalLog = console.log;
console.log = function(...args) {
originalLog('[LOGGED]', ...args);
// Send to analytics, save to file, etc.
};
console.log('Hello'); // Outputs: [LOGGED] Hello
This is useful for:
- Adding timestamps to all logs
- Sending logs to a remote server
- Disabling console in production
- Creating custom logging systems
Console Methods Are Not Standardized
Different browsers implement the Console API differently. Chrome has methods that Firefox doesn't, and vice versa. Always check compatibility:
// Some browsers don't have console.table
if (typeof console.table === 'function') {
console.table(data);
} else {
console.log(data);
}
Performance Impact
Console methods have performance costs, especially console.log() with large objects. In hot loops:
// Slow - logs 1,000,000 times
for (let i = 0; i < 1000000; i++) {
console.log(i);
}
// Faster - logs once
for (let i = 0; i < 1000000; i++) {
// work
}
console.log('Done');
Always remove debug logs from production builds, especially in performance-critical code.
Console Formatting (Chrome/Firefox)
You can style console output with CSS:
console.log('%cStyled Text', 'color: blue; font-size: 20px; font-weight: bold;');
console.log(
'%cError: %cSomething went wrong',
'color: red; font-weight: bold;',
'color: gray;'
);
Useful for:
- Making important logs stand out
- Creating ASCII art banners
- Color-coding different log types
Browser Differences to Know About
Chrome DevTools
- Best console.table() implementation
- Has console.memory
- Excellent object inspection
- Live expression watching
- Full async stack traces
Firefox Developer Tools
- Best for CSS debugging
- Great performance tools
- No console.memory (use about:memory instead)
- Different object formatting
Safari Web Inspector
- Clean interface
- Good for iOS debugging
- More limited than Chrome
- Some methods behave differently
Edge (Chromium)
- Similar to Chrome (same engine)
- Slightly different UI
- Generally compatible with Chrome examples
Takeaway: Test your debugging code in your target browsers. Don't assume all console methods work everywhere.
Creating Your Own Console Utilities
Once you master the built-in methods, create custom utilities:
const debug = {
// Quick object inspection
inspect(obj, depth = 3) {
console.group('🔍 Object Inspection');
console.dir(obj, { depth });
console.table(obj);
console.groupEnd();
},
// Performance timing
perf(label, fn) {
console.time(label);
const result = fn();
console.timeEnd(label);
return result;
},
// Conditional logging
when(condition, ...args) {
if (condition) {
console.log(...args);
}
},
// Memory-aware logging
memory(label) {
if (console.memory) {
const used = (console.memory.usedJSHeapSize / 1048576).toFixed(2);
console.log(`${label} - Memory: ${used} MB`);
}
}
};
// Usage
debug.inspect(complexObject);
debug.perf('calculation', () => heavyCalculation());
debug.when(isDev, 'Development mode active');
debug.memory('After data load');
Conclusion: Level Up Your Debugging Game
Here's the truth: most developers never learn these console methods. They stick with console.log() and wonder why debugging takes so long. You now know ten powerful methods that most of your peers don't:
- console.table() - See data structures at a glance
- console.time/timeEnd() - Measure performance precisely
- console.trace() - Track execution paths
- console.assert() - Catch bugs at the source
- console.group/groupEnd() - Organize complex logs
- console.dir() - Inspect objects deeply
- console.count/countReset() - Track function calls effortlessly
- console.clear() - Keep your console readable
- console.warn/error() - Add semantic meaning
- console.memory - Monitor memory proactively
Start using these tomorrow. Pick one method and integrate it into your workflow. Next week, add another. Within a month, you'll be debugging faster and more effectively than you ever thought possible.
Remember: good debugging isn't about working harder—it's about having better tools and knowing how to use them. The console API is one of the most powerful toolsets in your arsenal. Master it.
Now go forth and debug with confidence. Your future self (and your teammates) will thank you.
Have a favorite console method I didn't cover? Or a debugging technique you swear by? Drop a comment below—I'd love to hear what's working for you!
Top comments (0)