Print Debugging: Sometimes Console.log Wins
Learn: Print Debugging: Sometimes Console.log Wins
Welcome to TopperBlog! 👋
I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.
🎯 What I Write About:
• AI/ML Engineering & LLMs
• Web3 & Blockchain Development
• System Design & Architecture
• Interview Preparation (FAANG)
• Freelancing & Remote Work
• Modern Tech Stacks (Next.js, React, Rust, TypeScript)
• Performance Optimization & Best Practices
💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.
📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.
🌐 Let's connect and grow together in this amazing tech journey!
#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering
Print Debugging: Sometimes Console.log Wins
Or: How I Learned to Stop Worrying and Love the Humble Print Statement
The Debugger Shame
There I was, three hours deep into a production bug, my IDE's debugger attached, breakpoints scattered across my codebase like landmines. My senior colleague walked by, glanced at my screen, and asked, "Have you tried just... logging it?"
I felt personally attacked.
Here I was, wielding the full power of Chrome DevTools, breakpoints with conditional expressions, watch expressions evaluating complex object graphs, and this person—this person—was suggesting I use console.log like some kind of caveman banging rocks together.
But you know what? They were absolutely right.
The Dirty Secret Nobody Talks About
We've all been there. You're debugging a gnarly issue, and everyone expects you to fire up the debugger like a "real" developer. But here's the truth that'll get me kicked out of the Software Engineering Club: sometimes console.log() is genuinely the better tool.
Not because you're lazy. Not because you don't know how to use a debugger. But because in certain situations, print debugging is actually more effective.
This isn't about debugger vs. console.log tribalism. It's about using the right tool for the job, even when that tool feels embarrassingly simple.
When Debuggers Become Your Enemy
The Heisenbug Problem
You know what's worse than a bug? A bug that disappears when you try to observe it.
// This race condition only happens at full speed
async function processQueue() {
const item = queue.shift();
await processItem(item);
// Bug: sometimes item is undefined
// But when you step through with a debugger? Works perfectly.
}
Debuggers change timing. They pause execution. They give async operations time to complete. They turn your Heisenbug into a Schrödinger's bug—it exists and doesn't exist simultaneously, depending on whether you're observing it.
Meanwhile, console.log() just... runs. At full speed. In the real environment where the bug actually lives.
async function processQueue() {
console.log('Queue length:', queue.length);
const item = queue.shift();
console.log('Processing item:', item);
await processItem(item);
console.log('Item processed');
}
Boom. You immediately see that queue.length is 0 when the bug happens. Mystery solved in 30 seconds.
The "It Only Happens in Production" Nightmare
Picture this: Your app works flawlessly on localhost. Staging? Perfect. But production? Crashes harder than my motivation on Monday mornings.
You can't attach a debugger to production. Well, you can, but your DevOps team will hunt you down. Plus, good luck debugging a serverless function that lives for 100 milliseconds.
// Lambda function that fails mysteriously in production
exports.handler = async (event) => {
// Can't attach debugger here, buddy
const data = await fetchData(event.id);
return processData(data);
};
But you can log:
exports.handler = async (event) => {
console.log('Event received:', JSON.stringify(event));
const data = await fetchData(event.id);
console.log('Data fetched:', data ? 'success' : 'null');
const result = processData(data);
console.log('Processing complete:', result);
return result;
};
Those logs go to CloudWatch, Datadog, or wherever. You can see exactly what happened, when it happened, and why your function decided to ruin your weekend.
The Loop from Hell
Try debugging this with breakpoints:
for (let i = 0; i < 10000; i++) {
const result = complexCalculation(data[i]);
if (isSomehowWrong(result)) {
// Bug happens somewhere in here
// But WHERE?
}
}
Setting a breakpoint means clicking "continue" 10,000 times. Setting a conditional breakpoint means the debugger evaluates your condition 10,000 times, turning your loop into a slideshow.
Or you could:
for (let i = 0; i < 10000; i++) {
const result = complexCalculation(data[i]);
if (isSomehowWrong(result)) {
console.log(`Bug at iteration ${i}:`, { input: data[i], result });
}
}
Run it once. Get a complete list of every failure. Make coffee while it runs. This is the way.
The Async Debugging Hellscape
Modern JavaScript is async everything. Promises, async/await, callbacks, event emitters—it's turtles all the way down. And debuggers hate this.
async function fetchUserData(userId) {
const user = await db.users.findOne(userId);
const posts = await db.posts.find({ userId });
const comments = await db.comments.find({ userId });
return {
user,
posts: await Promise.all(posts.map(enrichPost)),
comments: await Promise.all(comments.map(enrichComment))
};
}
Try stepping through this with a debugger. You'll jump between files, lose context, forget what you were looking for, and question your career choices.
But with strategic logging:
async function fetchUserData(userId) {
console.log('→ Fetching user:', userId);
const user = await db.users.findOne(userId);
console.log('✓ User found:', user?.name);
const posts = await db.posts.find({ userId });
console.log('✓ Posts found:', posts.length);
const comments = await db.comments.find({ userId });
console.log('✓ Comments found:', comments.length);
console.log('→ Enriching posts...');
const enrichedPosts = await Promise.all(posts.map(enrichPost));
console.log('✓ Posts enriched');
console.log('→ Enriching comments...');
const enrichedComments = await Promise.all(comments.map(enrichComment));
console.log('✓ Comments enriched');
return { user, posts: enrichedPosts, comments: enrichedComments };
}
You get a beautiful narrative of exactly what's happening, in order, with timing information if you check the timestamps. It's like a story of your code's execution.
When Debuggers ARE Better (Let's Be Fair)
Look, I'm not a debugger hater. Debuggers are incredible for:
Exploring unfamiliar code: Stepping through someone else's codebase to understand flow? Debugger wins.
Inspecting complex object graphs: Need to drill into a deeply nested object? Debugger's interactive inspection beats console.log(JSON.stringify(obj, null, 2)).
Modifying state on the fly: Debuggers let you change variables mid-execution. That's genuinely magical.
Call stack analysis: When you need to know "how did I get here?", the call stack view is unbeatable.
But for quick iteration, production issues, timing-sensitive bugs, and "I just need to see what this value is"? Print debugging is your friend.
Level Up Your Print Debugging Game
If you're going to print debug (and you should), do it right.
1. Make Your Logs Searchable
// Bad: Generic logs that blend together
console.log('data:', data);
// Good: Prefixed, searchable logs
console.log('[UserService:fetchData]', data);
Now you can filter your console or grep your logs for [UserService:fetchData] and find exactly what you need.
2. Use Different Log Levels
console.log('Normal flow');
console.info('ℹ️ Interesting information');
console.warn('⚠️ Something smells funny');
console.error('💥 Everything is on fire');
Most logging systems let you filter by level. Use this power.
3. Log Context, Not Just Values
// Meh
console.log(userId);
// Better
console.log('Processing user:', userId);
// Best
console.log('Processing user:', {
userId,
timestamp: Date.now(),
source: 'webhook',
attempt: retryCount
});
Future you will thank present you for that context.
4. The Table Trick
const users = [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
{ id: 3, name: 'Charlie', role: 'user' }
];
// Instead of this mess:
console.log(users);
// Do this:
console.table(users);
You get a beautiful formatted table in your console. It's like Excel, but for debugging.
5. Trace Your Steps
function mysteryFunction() {
console.trace('How did I get here?');
// Shows you the full call stack
}
It's like a debugger's call stack, but in log form.
6. Time Your Operations
console.time('database-query');
const results = await db.query(sql);
console.timeEnd('database-query');
// Outputs: database-query: 234.567ms
No need for manual Date.now() math.
7. Group Related Logs
console.group('User Registration');
console.log('Validating email...');
console.log('Checking for duplicates...');
console.log('Creating user record...');
console.log('Sending welcome email...');
console.groupEnd();
Your logs become organized, collapsible sections. It's beautiful.
The Production-Ready Approach
"But wait," you say, "I can't leave console.log statements in production code!"
Says who?
// Use a proper logger
import logger from './logger';
logger.debug('Detailed debugging info');
logger.info('User logged in', { userId });
logger.warn('Rate limit approaching', { current, limit });
logger.error('Payment failed', { error, userId, amount });
In development, these go to your console. In production, they go to your logging service. You get the best of both worlds.
And here's the secret: good production logs ARE print debugging. You're just debugging issues that haven't happened yet.
The Real Wisdom
The best developers I know don't have a religious attachment to any one tool. They use debuggers when debuggers make sense. They use print statements when print statements make sense. They use rubber duck debugging when they need to explain their code to an inanimate object.
The goal isn't to use the "most sophisticated" tool. The goal is to fix the bug and move on with your life.
I've seen developers spend 30 minutes setting up a complex debugging session when two console.log statements would have solved it in 30 seconds. I've also seen developers add 50 log statements when one breakpoint would have shown them the issue immediately.
The trick is knowing which tool fits the situation.
The Actionable Takeaway
Next time you hit a bug, ask yourself:
- Is this timing-sensitive? → Print debugging
- Does it only happen in production? → Print debugging
- Is it in a loop or async chain? → Print debugging
- Am I exploring unfamiliar code? → Debugger
- Do I need to inspect complex objects? → Debugger
- Do I need to modify state to test something? → Debugger
And remember: the best debugging tool is the one that solves your problem fastest.
The Bottom Line
console.log() isn't a crutch. It's not a sign that you're a "bad" developer. It's a legitimate, powerful debugging technique that's been solving problems since the dawn of computing.
Dennis Ritchie used print statements. Linus Torvalds uses print statements. That senior engineer who makes three times your salary and writes code that actually works? They definitely use print statements.
So the next time someone gives you grief for using console.log(), just smile and remember: while they're still configuring their debugger, you've already fixed the bug and moved on to more important things.
Like arguing about tabs vs. spaces.
Now if you'll excuse me, I have about 47 console.log statements to remove from my codebase. Or maybe I'll just leave them. They might come in handy later.