
In brief
console.log prints a live reference to objects, which is why logged state seems to change after the fact. table, group, time and assert turn that noise into structured evidence.
Most debugging starts with console.log and ends there, which is a shame because the console has a dozen methods and the difference between them is the difference between reading a wall of text and reading an answer. This guide covers the methods worth knowing, the reference-versus-copy behaviour that makes logs look wrong, and how to read a stack trace when an async call fails silently.
Everything here runs in the UrsaKit JavaScript runner, which prints console output with levels, grouping and execution time.
01console.log is only the first tool
console.log writes whatever you give it, in order, with no structure. That is fine for a number and useless for a list of forty records. The rest of the API exists to add structure:
| Method | Use it for |
|---|---|
console.log | Anything; arbitrary values, formatted with %s / %d / %o |
console.table | Arrays of objects, rendered as a table |
console.dir | Inspecting one object's properties instead of its toString |
console.group / console.groupEnd | Nesting related output |
console.time / console.timeEnd | Measuring elapsed time between two points |
console.count | Counting how often a branch runs |
console.assert | Logging only when an assumption fails |
console.trace | Printing a stack trace at the current point |
02Objects are logged by reference
This is the behaviour that wastes the most time, because the console does not lie — it shows the live object.
const user = { name: 'Ada', plan: 'free' }
console.log(user) // expand later and it may read { name: 'Grace', plan: 'pro' }
console.log(user.name) // 'Ada' — a primitive is captured by value
user.name = 'Grace'
user.plan = 'pro'
In an interactive console the object view is built when you expand it, so it reflects the final state of the object, not the moment you called the method. Three ways to capture the value instead:
console.log(user.name) // log the primitive you care about
console.log(JSON.stringify(user)) // a string snapshot, readable in any console
console.log(structuredClone(user)) // a real deep copy, keeps types
Log the state you are comparing, not the whole object.
console.log('plan=' + user.plan + ' items=' + items.length)survives serialisation, reads the same in a server log and a browser, and cannot be mutated out from under you. Reserve full-object logging for exploratory work.
03Grouping and timing
Related output becomes readable when it is nested, and durations become comparable when they are measured instead of guessed.
console.group('checkout')
console.time('validate')
console.count('line') // line: 1
console.count('line') // line: 2
for (let i = 0; i < 1e6; i++) {
/* work */
}
console.timeEnd('validate') // validate: 12.4ms
console.groupEnd()
A few details that matter in practice:
console.timeandconsole.timeEndmust use the same label, and each label must be ended before it can be started again.console.countstarts each label at zero and prints the running total, which is the quickest way to prove a render function runs twice.console.groupCollapsedis the same asconsole.groupbut starts closed, which keeps a long run readable.
Timing measured this way includes everything between the two calls, so use it for relative comparisons — before and after a change — rather than as a benchmark. The runner reports the total execution time of the snippet as well.
04Assertions and counting
console.assert is the inverse of console.log: it stays quiet while your assumption holds.
function applyDiscount(price, percent) {
console.assert(
percent >= 0 && percent <= 100,
'discount out of range',
percent
)
return price * (1 - percent / 100)
}
applyDiscount(100, 20) // no output
applyDiscount(100, 150) // Assertion failed: discount out of range 150
Inside a loop, an assertion is a filter: you get output only for the records that break the rule, instead of scrolling past the ones that do not.
05Reading a stack trace
When you need to know which caller reached a function, print a trace instead of adding a log to every candidate:
function parse(input) {
console.trace('parse called')
return JSON.parse(input)
}
parse('{"a":1}')
// parse called
// at parse (<anonymous>:2:11)
// at <anonymous>:4:1
For thrown errors, the stack is the map from the failure back to the call site. Read it top-down: the first line is where the error was thrown, the frames below are its callers. The frame that belongs to your code — not to a library or the runtime — is usually the one to fix.
try {
JSON.parse('{ "a": 1, }')
} catch (error) {
console.error(error.name) // SyntaxError
console.error(error.message) // Expected double-quoted property name in JSON at position 9
console.error(error.stack) // where it happened, frame by frame
}
06Async errors that never reach a log
An error thrown inside an async function becomes a rejected promise. If nothing handles it, the failure can happen without ever hitting the line you were watching.
async function load() {
throw new Error('network down')
}
load() // unhandled rejection: nothing catches this
Two habits make these visible:
- Await inside try/catch, so the failure lands on a line you control.
- Handle rejections at the boundary with
.catch()on fire-and-forget calls, or listen for the runtime's rejection event in a page you own.
async function load() {
throw new Error('network down')
}
try {
await load()
} catch (error) {
console.error('load failed:', error.message) // load failed: network down
}
Do not ship verbose console output.
Debug logs in production leak internal state to anyone who opens the console, and logging large objects on a hot path costs real time. Gate them behind a flag or remove them before the build.
07Practising in a sandbox
- Paste a snippet and run it — The JavaScript runner parses the code first and reports syntax errors with a line number, so you find typos before running anything.
- Read the levelled output — Log, warn and error lines are distinguished, and the console groups output the same way the browser does.
- Watch the execution timer — The reported duration is the honest cost of the snippet, which is enough to compare two approaches.
- Try the failing branch — Re-run with input that breaks the assumption, because the assertion you never triggered is the one that would have caught the bug.
The console is a measurement tool, not a notepad. Using the right method is what turns a page of output into a single line that answers the question.
Run snippets in a sandboxed console with syntax checking, execution time and levelled output.
JavaScript RunnerFrequently asked questions
Why does console.log show the wrong value for my object?
The console stores a live reference to the object, not a copy. If the object is mutated after the log call, the expanded view shows the current state. Log a primitive, a JSON string or a structuredClone copy to capture the value at that moment.
What does console.table actually do?
It renders an array of objects as a table, using the object keys as columns. It is the fastest way to read a list of records, and it accepts a second argument to limit the columns.
When should I use console.assert?
When you want output only if an assumption is violated. console.assert(condition, message) writes nothing while the condition is truthy, which keeps the console clean in normal runs.
Is console the same in Node.js and the browser?
Mostly. table, group, time, count and assert exist in both, but the browser renders interactive objects while Node prints inspect output, and Node adds process-level events such as unhandledRejection.
Do console calls slow down production?
Logging large objects does have a cost, and keeping verbose logs in production leaks data into a place users can read. Remove or gate debug logging before shipping.
How do I see a stack trace without throwing an error?
Call console.trace(). It prints a stack trace at the point of the call, which is how you find which of five callers is responsible.
Related articles
- How to format, validate and minify JSON
Turn an inspected object into something you can read and diff.
- Regex groups, lookarounds and backtracking
Test the pattern you are debugging in the console against real strings.
Related tools
- JavaScript Runner
Sandboxed console with timing and levelled output.
- JSON Formatter & Validator
Pretty-print the object you dumped.
- HTML Online Tool
Render markup your script produces.
- Regex Tester
Check the pattern your code depends on.