You know console.log. That's about 10% of what's available to you.
Here's the other 90%.
Console Methods You're Not Using
console.table turns arrays and objects into readable tables instantly:
const users = [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
{ id: 3, name: 'Carol', role: 'user' }
]
console.table(users)
// Formatted table with columns for id, name, role
// Immediately reveals missing fields, wrong types
console.table(users, ['name', 'role'])
// Show only specific columns
console.trace shows how execution reached a function:
function processPayment(order) {
console.trace('processPayment called')
// Stack trace reveals the full call chain
// Instantly answers "who called this and why?"
}
console.group organizes complex flows:
async function handleCheckout(cart) {
console.group('Checkout Flow')
console.log('Cart items:', cart.items.length)
console.group('Validation')
const isValid = validateCart(cart)
console.log('Valid:', isValid)
console.groupEnd()
console.group('Payment')
const result = await chargeCard(cart.total)
console.log('Result:', result.status)
console.groupEnd()
console.groupEnd()
}
// Produces collapsible nested output
// No more scrolling through 200 flat log lines
console.time pinpoints performance bottlenecks:
console.time('api')
const data = await fetch('/api/users').then(r => r.json())
console.timeEnd('api')
// Output: api: 247.3ms
console.time('render')
renderList(data)
console.timeEnd('render')
// Output: render: 12.1ms
// Now you know the API is slow, not the render
console.assert logs only when something is wrong:
function processUser(user) {
console.assert(user !== null, 'User is null!', { user })
console.assert(user.email, 'Missing email!', { userId: user.id })
console.assert(user.role, 'Missing role!', { userId: user.id })
// Silent when everything is fine
// Loud when something breaks
}
Debugger Beyond Breakpoints
Stop clicking line numbers. Use conditional and programmatic breakpoints.
Conditional breakpoints fire only when you need them:
// Right-click a breakpoint in DevTools, add condition:
userId === "abc-123" && order.total > 100
// Now it only pauses for the specific case you're investigating
// No more stepping through 500 irrelevant iterations
Programmatic debugger with conditions:
function processOrder(order) {
if (order.total < 0) {
debugger // Only pauses for the suspicious case
}
// Normal execution continues uninterrupted
}
logpoints instead of console.log (Chrome DevTools):
Right-click a line number → Add logpoint → Type an expression.
It logs without modifying your code. No more forgetting to remove debug statements before committing.
The Network Panel Tricks
Filter by status to find failures fast:
Type status-code:500 in the Network filter. Only failing requests show up.
Block requests to test error handling:
Right-click any request → Block request URL. Your app now behaves as if that API is down. Does your error handling actually work?
Copy as fetch for reproduction:
Right-click any request → Copy → Copy as fetch. Paste in console. Now you can replay and modify any API call without touching your code.
Throttle specific requests:
Chrome DevTools → Network → Throttling. Simulate slow 3G to test loading states and timeouts. Most "works on my machine" bugs appear instantly.
Finding React Bugs Fast
Why did this component render?
// Temporary debugging hook
function useWhyDidYouRender(name, props) {
const prev = useRef(props)
useEffect(() => {
const changes = {}
Object.entries(props).forEach(([key, val]) => {
if (prev.current[key] !== val) {
changes[key] = { from: prev.current[key], to: val }
}
})
if (Object.keys(changes).length > 0) {
console.log(`[${name}] re-rendered because:`, changes)
}
prev.current = props
})
}
// Usage
function UserCard(props) {
useWhyDidYouRender('UserCard', props)
return <div>{props.name}</div>
}
Catch the stale closure before it catches you:
// The bug everyone hits
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
console.log('count is:', count) // Always 0. Stale.
setCount(count + 1) // Always sets to 1
}, 1000)
return () => clearInterval(id)
}, [])
// The fix
useEffect(() => {
const id = setInterval(() => {
setCount(prev => {
console.log('count is:', prev) // Always current
return prev + 1
})
}, 1000)
return () => clearInterval(id)
}, [])
}
Find event listener leaks:
// This creates a new listener on EVERY render
function Search() {
useEffect(() => {
document.addEventListener('keydown', handleKey)
// Missing cleanup = memory leak + ghost handlers
})
// After 10 re-renders, handleKey fires 10 times per keypress
// Fix: add cleanup and deps
useEffect(() => {
document.addEventListener('keydown', handleKey)
return () => document.removeEventListener('keydown', handleKey)
}, [handleKey])
}
// Verify in DevTools:
// Elements → select node → Event Listeners panel
// If you see duplicates, you have a leak
The Git Debugging Weapons
git bisect finds exactly which commit introduced a bug:
git bisect start
git bisect bad # Current commit is broken
git bisect good abc123 # This old commit was working
# Git checks out the middle commit
# You test: is the bug here?
git bisect good # or git bisect bad
# Repeat. Git binary searches through history.
# Finds the exact breaking commit in O(log n) steps.
# 1000 commits? Found in ~10 steps.
git blame for context:
git blame src/components/UserList.jsx
# Shows who wrote each line and when
# That weird line on line 47?
# Written 2 years ago by someone who left
# Now check the commit message for context
git log -1 --format="%B" abc123
# "Hotfix: prevent crash when API returns empty array"
# Now you know WHY line 47 looks weird
Search for when a function was changed:
git log -p -S "calculateTotal" -- "*.js"
# Shows every commit that added or removed "calculateTotal"
# Find when it was introduced, modified, or broken
The 5 Step Process
Every bug. Every time.
1. WHAT exactly is the symptom?
→ Not "it's broken." Specific observable behavior.
2. CAN I reproduce it?
→ If not, gather more context until I can.
3. WHEN did it last work?
→ git log --oneline --since="last week" -- src/
4. WHAT are three possible explanations?
→ Force yourself to think of three before investigating.
5. WHAT is the fastest experiment to eliminate one?
→ Design a test, not a guess.
Six hour sessions become twenty minutes.
Not talent. Process.
Full debugging framework with more patterns and examples: jsgurujobs.com
Top comments (0)