How to debug Node.js apps
Effective debugging enables quick identification and resolution of bugs, performance issues, and unexpected behaviors in Node.js applications. As the creator of CoreUI, a widely used open-source UI library, I’ve debugged complex Node.js backend services throughout my 11 years of backend development. The most practical approach combines console logging for quick insights with Node.js inspector and debugger statements for deep investigation. This method provides flexibility from simple logging to full breakpoint debugging with variable inspection and call stack analysis.
Use console methods for quick debugging and debugger statement with –inspect flag for breakpoint debugging.
// Basic console debugging
const express = require('express')
const app = express()
app.get('/api/users', (req, res) => {
console.log('GET /api/users called')
console.log('Query params:', req.query)
const users = fetchUsers(req.query.page)
console.log('Found users:', users.length)
res.json(users)
})
function fetchUsers(page = 1) {
console.log(`Fetching users for page: ${page}`)
// Using console.table for structured data
const users = [
{ id: 1, name: 'John', role: 'Admin' },
{ id: 2, name: 'Jane', role: 'User' }
]
console.table(users)
return users
}
// Using debugger statement
function processOrder(order) {
console.log('Processing order:', order.id)
debugger // Execution will pause here when running with --inspect
const total = calculateTotal(order.items)
if (total > 1000) {
debugger // Conditional breakpoint location
applyDiscount(order)
}
return total
}
function calculateTotal(items) {
console.time('calculateTotal')
const total = items.reduce((sum, item) => {
console.log(`Adding item: ${item.name} - $${item.price}`)
return sum + item.price
}, 0)
console.timeEnd('calculateTotal')
console.log('Total calculated:', total)
return total
}
// Advanced debugging with trace
function deepFunction() {
console.trace('Execution path to deepFunction')
return 'result'
}
// Conditional logging
const DEBUG = process.env.NODE_ENV === 'development'
function log(...args) {
if (DEBUG) {
console.log('[DEBUG]', ...args)
}
}
app.get('/api/debug-test', (req, res) => {
log('Debug endpoint called')
log('Request headers:', req.headers)
deepFunction()
res.json({ message: 'Check console for debug output' })
})
app.listen(3000, () => {
console.log('Server running on port 3000')
console.log('Start debugging with: node --inspect server.js')
})
Running with debugger:
# Start with inspector enabled
node --inspect server.js
# Start with inspector that breaks on first line
node --inspect-brk server.js
# Specify custom inspector port
node --inspect=9229 server.js
Here console.log provides quick output for tracking execution flow and variable values. The console.table method displays arrays and objects in readable table format. The debugger statement creates breakpoints that pause execution when running with –inspect flag. The console.time and console.timeEnd pair measures execution duration for performance debugging. The console.trace shows the complete call stack leading to current execution point. Environment-based conditional logging prevents debug output in production. Different console methods (log, warn, error, info) provide visual categorization in terminal output.
Best Practice Note:
This is the debugging foundation we use in CoreUI Node.js backend services before implementing advanced debugging tools for production environments. Remove or disable debugger statements before deploying to production to prevent performance issues, use environment variables to control debug logging levels, and integrate with logging libraries like Winston or Bunyan for structured logging with log levels, timestamps, and centralized log management in production systems.



