The console Module
console is the logging object you already know from the browser, but Node's version writes to the process's standard streams and ships several methods beyond log that are genuinely useful for debugging and diagnostics. Knowing how it really works — which stream each method targets, when it blocks, and why it is not a real logger — saves you from a class of production surprises.
stdout vs stderr
console.log / console.info write to stdout; console.error / console.warn write to stderr. The global console is really just a Console instance bound to process.stdout and process.stderr. Keeping errors on stderr lets you redirect the two streams separately — essential for logging pipelines:
node app.js > out.log 2> err.log # stdout → out.log, stderr → err.log node app.js 2>&1 | tee all.log # merge both, also print to terminal
Format specifiers
The first argument can contain printf-style placeholders, filled by the remaining arguments. Anything left over is appended, space-separated:
console.log('User: %s, Age: %d', 'Ada', 36) // User: Ada, Age: 36
console.log('As JSON: %j', { a: 1 }) // As JSON: {"a":1}
console.log('%o', { nested: { deep: true } }) // %o = inspect with options
console.log('Object:', { a: 1, b: [2, 3] }) // no specifier → pretty-print
console.log('a', 'b', 'c') // a b cSpecifier | Formats as |
|---|---|
| String |
| Integer |
| Float |
| JSON ( |
| Object via |
| CSS — ignored in Node, consumed in browsers |
Controlling object output with util.inspect
By default Node only expands objects two levels deep and truncates long output. For deep structures, format explicitly:
import { inspect } from 'node:util'
console.log(deepObj) // truncated at depth 2 → [Object]
console.log(inspect(deepObj, { depth: null, colors: true })) // full tree, colorizedBeyond log: the useful methods
console.table([ // render arrays/objects as a grid
{ name: 'Ada', role: 'admin' },
{ name: 'Linus', role: 'user' },
])
console.dir(obj, { depth: 2 }) // inspect with options
console.assert(1 === 2, 'math broke') // logs to stderr ONLY if assertion is false
console.count('hits') // hits: 1, hits: 2, ... (keyed counter)
console.trace('got here') // prints a stack trace at this point
console.group('Request'); console.log('...'); console.groupEnd() // indent nesting┌─────────┬─────────┬─────────┐ │ (index) │ name │ role │ ├─────────┼─────────┼─────────┤ │ 0 │ 'Ada' │ 'admin' │ │ 1 │ 'Linus' │ 'user' │ └─────────┴─────────┴─────────┘
Measuring time
console.time('query')
await runQuery()
console.timeEnd('query') // query: 42.318ms — must use the same labelquery: 42.318ms
Custom Console instances
You can construct a Console bound to any writable streams — e.g. to log to files instead of the terminal:
import { Console } from 'node:console'
import fs from 'node:fs'
const logger = new Console({
stdout: fs.createWriteStream('./out.log'),
stderr: fs.createWriteStream('./err.log'),
})
logger.log('written to out.log')
logger.error('written to err.log')Is console output synchronous?
Use
console.error/warnfor problems so they land on stderr.console.table,console.time,console.trace, andconsole.dirare debugging gold.Remember output can block (TTY/file) or be lost on instant exit (pipe).
Swap to a structured logger for production.