Devpuff
Pricing
Log InStart Free
  1. Home
  2. Blog
  3. Beyond console.log: the console methods worth knowing
Tools

Beyond console.log: the console methods worth knowing

console.log answers what is this value. Seven other methods answer how many times, how long, who called it, and which of these rows is wrong.

By Max Arthur
Co-Founder & Content Marketer·August 12, 2026·8 Min read
A console showing a table of objects, a counted label, and a timing result beside a plain log of the same data

console.log answers one question: what is this value right now. Most debugging asks a different question, and the console already has a method for it.

Question Method
Which of these rows is wrong? console.table
How many times did this run? console.count
How long did this take? console.time / timeEnd
Who called this function? console.trace
Is my assumption still true? console.assert
Why is this output such a mess? console.group
What is this DOM element, as an object? console.dir

None of them need a library or a build step. Every output below was captured from a real browser console.

console.table

The highest return of any of these. Given an array of objects, it prints a real table with sortable columns instead of a stack of collapsed blobs:

console.table([
  { name: "Ada", age: 36 },
  { name: "Grace", age: 45 },
]);

Ten API results as console.log is ten expandable arrows you have to open one at a time. As console.table it is ten rows, and a wrong value is visible immediately because it sits in a column with its neighbours.

You can limit the columns, which is what makes it usable on real data:

console.table(users, ["id", "email"]);

Objects of objects work too, using the keys as row labels. It is the right first move any time you are looking at more than about three records.

console.count

Answers "did this run, and how many times", without you maintaining a counter variable:

console.count("hit");
console.count("hit");
console.count("hit");
hit: 1
hit: 2
hit: 3

The label is optional and defaults to default. console.countReset("hit") sets it back to zero.

This is the fastest way to check whether a handler is bound twice, whether an effect runs on every render, or whether a code path executes at all. A console.log("here") tells you it ran; console.count("here") tells you it ran eleven times, which is usually the more interesting fact.

console.time and console.timeEnd

Measures elapsed time between two points, with no arithmetic on your part:

console.time("t");
for (let i = 0; i < 1e5; i++);
console.timeEnd("t");
t: 3.710205078125 ms

The labels must match. console.timeLog("t") prints the running total without stopping the timer, which is useful for finding the slow step inside a longer operation.

One caveat worth stating plainly: this measures wall-clock time on one run, on your machine, with your extensions loaded and DevTools open. It tells you that one thing is much slower than another. It is not a benchmark, and small differences between two runs mean nothing.

console.trace

Prints the call stack at the point it runs, without throwing anything:

function save() {
  console.trace("save called");
}

The output is the chain of calls that led here. This is the tool for "why is this running at all", and it beats reading the code when a function is called from several places or from inside a library.

It is also the answer to "which of my components triggered this fetch", which is a question console.log genuinely cannot answer.

console.assert

Logs only when the condition is false:

console.assert(1 === 1, "never shown");
console.assert(1 === 2, "assertion failed: 1 is not 2");

Only the second line appears. That inversion is the point. A console.log of a value you expect to be fine adds noise on every run and you stop reading it. An assertion is silent while the assumption holds and speaks up exactly when it breaks.

It is well suited to invariants inside loops:

console.assert(row.id != null, "row without an id", row);

Note that it does not stop execution, unlike a test assertion. It only reports.

console.group

Nests related output so a busy console stays readable:

console.group("group A");
console.log("inside");
console.groupEnd();

The lines between them are indented and collapsible. console.groupCollapsed starts the group closed, which is what you want when logging inside a loop: one line per iteration that you can open only if it looks interesting.

This is the difference between a console you scroll past and one you can actually read.

console.dir

console.log(element) on a DOM node prints the rendered HTML. console.dir(element) prints it as a JavaScript object, with every property expandable.

Use dir when you want to know what the node is, such as its event listeners, its dataset, or its current value, rather than what it looks like.

The trap in logging objects

This is the one that costs people real time, and it comes from console.log being lazy.

When you log an object, the browser stores a reference, not a snapshot. Expanding it later in DevTools shows the object's state at the moment you expanded it, which may be long after the log ran. So an object that was correct when logged can appear wrong, and an object that was wrong can appear to have fixed itself.

Take an explicit snapshot when the value will change:

const o = { n: 1 };
console.log("snapshot", JSON.stringify(o));   // {"n":1}
o.n = 2;
console.log("live ref", JSON.stringify(o));   // {"n":2}

Both lines were captured, and each shows the value at the moment it ran. structuredClone(o) does the same while keeping it an object you can expand.

If you have ever logged a value, seen it look correct, and been unable to explain the bug, this is a strong candidate for why.

Two more habits

Log objects, not values. console.log({ user, cart }) prints the names alongside the values, so a screen of output stays identifiable. console.log(user) followed by console.log(cart) gives you two anonymous blobs.

Use the styled form for anchors. A console.log("%c START", "color: red; font-size: 16px") line is findable when you are scanning hundreds of entries.

Node and the browser are not the same console

Most of these methods exist in both, and three behave differently in ways that matter.

The reference trap is a browser problem. Node serializes the object when the log runs, so the same test that misleads you in DevTools prints honestly:

logged before mutation: { n: 1 }
after mutation: { n: 2 }

Both lines show the value at the time they ran. So if you have been burned by this in the browser, you do not need the JSON.stringify habit in server code.

Node truncates deep objects. Anything past two levels collapses:

console.log({ a: { b: { c: { d: 1 } } } });
// { a: { b: { c: [Object] } } }

[Object] is not an error and not an empty object, it is Node declining to go deeper. This one wastes real time, because it looks like your data is missing. The fix:

console.dir(obj, { depth: null });

%c styling does nothing in Node. The CSS argument is consumed and no colour appears, so styled log helpers written for the browser silently degrade on the server.

console.table does work in Node, printing a proper bordered table, and so does console.count. Those two are worth using in server code as much as in the browser.

Log levels, and why they matter more than they look

console.log, info, warn and error are not four names for the same thing. The browser treats them differently, and using them properly makes a busy console filterable.

warn and error capture a stack trace and appear under the Warnings and Errors filters. debug is hidden unless you enable Verbose. Since DevTools lets you filter by level, a codebase that uses all four gives you a console where you can hide the routine chatter and see only what went wrong.

Using console.log for everything throws that away, and it is the reason so many consoles are unreadable in the first place.

The ones to skip

Not everything on the console object earns its place:

  • console.clear in committed code is hostile. It erases output somebody else was reading, including logs from before your code ran.
  • console.error for expected conditions. A handled 404 is not an error, and marking it as one trains people to ignore red text.
  • console.log left in production. It leaks internal shape to anyone with DevTools open, and logging a large object keeps it alive in memory because the console holds a reference to it. That is a real, if small, leak, and it is the same reference behaviour described above.

Strip logs at build time rather than by hand. Most bundlers can drop specific console methods, which lets you keep warn and error while removing the rest.

When to stop logging and use the debugger

All of the above share a limitation: you have to know what to log before you run the code. A breakpoint does not.

Set one in the Sources panel, or write debugger; in the code, and execution pauses with every variable in scope available to inspect. You can then step through line by line and watch values change, which answers "where does this go wrong" rather than "what is this value here".

Two features make it worth the switch:

  • Conditional breakpoints. Right-click a breakpoint and give it a condition such as id === 42, and it pauses only on the iteration you care about. This replaces the entire pattern of wrapping a log in an if.
  • Logpoints. Same menu, but instead of pausing it prints an expression. That is a console.log you did not have to add to the source, and did not have to remember to delete.

The rule of thumb: reach for a log when you have a specific question about a specific value, and for the debugger when you do not yet know what to ask. If you are on your third round of adding logs and re-running, you wanted a breakpoint two rounds ago.

For getting around the panels themselves, Chrome DevTools for beginners covers the 20 percent you use daily.

Want somewhere to practise reading output rather than guessing at it? Start with the JavaScript track.

Keep reading

More from the blog

One commit from a feature branch being copied onto main as a new commit with a different hash
September 5, 2026·8 min readTools

git cherry-pick: move one commit anywhere

Cherry-pick copies a commit onto your current branch. It creates a new commit with a new hash, so the original stays where it was.

Read more
A successful 200 response arriving at the browser and being stopped before it reaches the page's JavaScript
September 3, 2026·7 min readTools

CORS error: what the browser is actually blocking

A CORS error is the browser refusing a response the server already returned with a 200. Here is that proved with two real origins, and the fixes.

Read more
{ }
✦

Ready to write some code?

Put this into practice - start your first free lesson. No setup, no credit card.

Start learning free
Devpuff

Learn to code by doing. One tiny, playful lesson at a time.

Learn
ProgramsCoursesPricing
Company
AboutBlogResourcesAffiliates
Support
Help CenterContactStatus
Programs
Frontend DevelopmentFull-Stack DevelopmentPython Developer
Courses
Advanced ReactAsync JavaScriptAsync PythonCSS BasicsCSS LayoutDSA Basics
Learn to Code
Learn JavaScriptLearn PythonLearn SQLLearn HTMLLearn CSSLearn ReactBrowse All Topics
Platform Comparisons
Devpuff vs CodecademyDevpuff vs MimoDevpuff vs Sololearn
© 2026 Devpuff. All rights reserved.Privacy PolicyTerms and ConditionsCookies PolicyRefund Policy
Devpuff vs freeCodeCamp
Read All Comparisons