155 comments

[ 3.1 ms ] story [ 189 ms ] thread
Another trick that might be more practical for actually debugging is using the object shorthand. For example, instead of...

    console.log(x, y);
which contains the information you need, but lacks any useful context, try...

    console.log({x, y});
...which will print out like an object, including the key names.
Unfortunately that tends to print the object with collapsed values, requiring you to expand the object to actually see any of the values.
I much prefer it being collapsed. When dealing with large objects, the firehose can be annoying.
Then, one day, after 12 hours chasing a race condition bug, you learn that when you expand objects from console.log, it shows you the current value of properties and not the values at the time of the console log. Because the values are evaluated when you click on the arrow.

Then you quit web development and start a new career.

I was just about to mention this, I wasted a solid hour or two on this until I realised what was happening.
As I pointed out in another comment:

  console.log({x, y});
the wrapping object is being created at log time, so its values will never be changed after the fact. That could still happen with the contents of x or y themselves, but then it's no different from the original way (console.log(x, y);)
We can actually just test this. Here's some code:

    const x = {value: 0};
    console.log(x);
    console.log({x});
    x.value = 1;
Running that in the latest Chrome javascript console, we see that the first version prints `{value: 0}` and the second prints `{x: {...}}`. When you expand the second one, it will show `{x: {value: 1}}`.
yep, but then when I expand (click triangle) the {value: 0} line, it shows "value: 1" on the next line, only to rise the confusion.

To be fair, there is also an "i" in a square, reminding me about this behaviour.

The really fun part is doing this :)...

Evaluate the expression:

    const x = {value: 0};
    console.log(x);
    console.log({x});
    x.value = 1;
You get this:

    {value: 0}
    {x: {…}}
Expand the first arrow of the `x:`:

    v {x: {…}}
     > x: {value: 1}
Now evaluate:

    x.value = 3
Then expand the second arrow:

    v {x: {…}}
     > x:
         value: 3
Now if you unexpand the arrow, you get 1, but if you expand it you get 3 =)... (Well at least in chrome)
> Now if you unexpand the arrow, you get 1, but if you expand it you get 3 =)...

Moreover, if you expand arrow next to {value: 0}, you will see literally this:

    v {value: 0} [i]
        value: 3
> its values will never be changed after the fact

By this I mean, the x and y in the output will never themselves change value. They may be mutated, but they cannot be reassigned in the printed object. The printed object is exclusively referenced by the console itself, even if the nested objects within it may be referenced elsewhere.

> That could still happen with the contents of x or y themselves, but then it's no different from the original way (console.log(x, y);)

By this I mean exactly what you demonstrated, the point being that it had nothing to do with the original suggestion made by jchw.

Rite of passage for every web developer. The moment when you realize this is the moment when the universe makes sense again... after long hours of total confusion.
Yeah I don't remember how I found about this, but I remember it involved some `console.log(JSON.stringify(...))`. It's tricky to notice.
And then you can't repro the bug anymore because it was related to Mobx observables or whatever and that JSON.stringify suddenly made it work so you just... leave it there...
Fun story explaining this to the PR reviewers though :D
You are not wrong. Is this a repl or a debugger? Because it seems to be mixture? How is that helpful?!
FWIW, it's not like it could work in any other way.

When you console.log() an object, it just stores a pointer to the object, and decorates it with an interactive label containing some text, so that it looks nice. This is fast to do, and most of the time, it's exactly what you want.

To log a static snapshot with equivalent interactive expansion capability, console.log() would have to do a general deep copy of your object - it would have to walk every pointer in the object and replace the pointee with its own recursive full deep copy, making sure to detect and handle cycles well, and keeping track of every replacement to ensure referential integrity (e.g. if two random objects A and B both have a pointer to the same object C, the copied A' and B' better both point at the same C'). An unlucky console.log() could easily copy half of your heap into the console, and god help you if you logged a DOM element.

(Also, all these copies would not be garbage-collected until you cleared the console.)

An universal deep copy is impractical to implement (notice how nobody seems to ever implement it, at all, in any programming language), and having console.log() do one would be an incredibly powerful and unpredictable footgun. Meanwhile, if you want to log a static snapshot of an object, all you need to do is to write console.log(cloneForLog(object)), where cloneForLog() is a function you wrote that does whatever copying is appropriate in your situation.

I think the only bad thing about console.log() is that this behavior is not taught to people as a core and important aspect of the function. I guess maybe if console.log() was restricted to strings, and something like console.logPresentation() was a separate function for printing objects, people would check the docs first and wouldn't be surprised.

> To log a static snapshot with equivalent interactive expansion capability, console.log() would have to do a general deep copy of your object

Not necessarily, and if it did I don't think it would address the actual problem.

console.log could do all the presentation work upfront, right then and there when you log, and provide a collapsed view of that. This would have to include detecting and cycling handles, as it would have to do for a deep copy as you mentioned. The cost would be wasting cycles on this work even if nobody looks at its result.

But where it gets dangerous is if there's any side effects in following the object tree. If visiting for logging e.g. creates nodes, or changes them, or whatever. Arguably "you get what you ask for", but this and the performance hit for generating log output before anyone really looks at it deeply, are probably the main reason things are as they are.

Err, handling cycles, not cycling handles. Can't edit anymore, but funny anyway...
Technically it's V8/DOM <-> CDP <-> devtools JS -> devtools DOM -> Skia displaylist.

In the current status quo, devtools requests values through CDP as you expand the nodes.

Based on experiences with using Expand recursively on surprisingly short JSON network responses, I get the impression either the CDP I/O or the JS driving it is quite slow. Or the implementation's just accidentally quadratic.

In any case, I get the impression the right solution would be to make V8 execute and retain the deep-copy internally, then forward bits of it over CDP as requested.

Hrm, now I'm curious if the underlying mechanics that power the HeapProfiler could be readily repurposed for this.

The only fundamental issue, which is likely been the central blocker all along, is representing objects that are cyclic; the implementation would be closer to "object snapshot" than "literal deep copy".

(CDP = chrome devtools protocol, ie what gets exposed over --remote-debugging-port.)

Excuse me?! I just tried this out and I honestly never knew this until now. I can only imagine how much this has caused me trouble in the past.
If they're primitives most consoles will show you the values without expansion
You could do console.table({x, y}) if you really want to see them initially expanded
On Node, console.dir(x, {depth: null}) will print the object fully expanded (even in cases where console.log by itself won't). Pass something other than `null` to `depth` and it'll recurse to that depth, as well.

Doesn't help on browser, though; the options arg to console.dir isn't part of the standard there.

You can console.log(JSON.stringify(x, null, 2)) to pretty-print the object as JSON. It doesn’t look as good, but it’ll expand and show you the value as it was the moment you logged it. (As opposed to the value when you open it in dev tools)
thank you. hadn't seen that before
Or you can console.log(JSON.stringify(x, null, 2));
I think you misunderstood the intent of the GP
But this way is easier to reconcile the output, because the values logged shown are what they where at the time of the console.log(), not at the time of expansion (later).

Try this in a browser console: x={a:1,b:{c:1}};console.log(x);x.b.c=2;

then 'expand' the object, 'c' will be logged as 2, not 1

The original commenter was creating the object at log-time, though, which means it wouldn't be shared by anything else. Unless x or y is an object, but in that case the issue is completely tangential to the original suggestion

And anyway- JSON.parse(JSON.stringify(x)) would be preferable because you'd still get the browser's rich object exploration

> Unless x or y is an object, but in that case the issue is completely tangential to the original suggestion

Of course we have to assume that x and y may be objects!

Posted this and then realized you beat me to it :)
I really like this method but unfortunately most debuggers print objects with keys in alphabetical order so there’s no way to get the keys you care about most at the top. Is there a way to rectify this?
This is going to make it significantly more janky but why not just put it into a list inside the curly brackets? That should preserve order while still triggering the object debugging feature
The best trick for actual debugging is to leverage your browser console.

In a local build you basically have a 1:1 mapping of source code to deployed code and the debugger here basically becomes your IDE. You can hit Ctrl+Shift+P in the inspector and use the same kind of fuzzy file matcher you have in Sublime Text or VS Code, and from there you can set breakpoints, modify the code in memory, and so on. The console will reflect on the entire scope and annotate the code with their runtime values, the same as you get when working inside a JetBrains IDE.

But it's JS, so you can tweak it without committing it to disk and so you get some form of REPL driven development. I can't remember the last time I've used a console.log over setting a breakpoint in the debugger and fucking with the application state at that point to understand an issue.

You can get quite close to that after you've bundled your code and deployed it to a server, so long as you've got good source maps going on.

Print debugging is invaluable, but I can't help but think there's something of Smalltalk or Lisp in how you can mess with your app within a sandbox through the inspector, at runtime. The only thing that breaks the model is the transpilation and minification, without sourcemaps.

FYI VS Code's JS debugger speaks Chrome Debugger Protocol and allows you to use the built in Debug Console just as you would the browser's debug console, also it's possible to set browser breakpoints/etc., and you get inline value hovers.
I bet the last time you used console.log was a race condition bug then :-)
I keep on coming back to debugging with print statements. I (embarrassingly) started using console.trace more often only recently.
Nice little article. Learned a new trick or two.

I'd love to see one on the step-thru debugger!

I often always do this to deep print my javascript object. console.log(JSON.stringify(myObject, null, 4));
This is great trick because the console will keep a reference to your object, not a copy, so if it changes between the time it was logged and now then the log expando shows the new value, not the old value.

By printing out the object you get a point-in-time snapshot rather than a reference to a mutable object.

(comment deleted)
I used to think `console.log` was only good for soothing and assuaging the limbs of trees. Now I know better!
This hurt to read. Don't get me wrong, I salute you for it, but it still hurt...
If you're using console.log to do debugging then it's worthwhile giving yourself a little more data to point at where a problem might lie - timings.

Open up devtools (cmd+option+j), then open the command palette (cmd+shift+P), and then search for "console", and then select "Console - Show Timestamps". Now every console output will have the high definition timestamp prepended to it. That can be really helpful if you don't want to go down the whole perf chart rabbit hole, or if you think things might be running in the wrong order due to some async weirdness.

(This probably only works in Chrome)

In Firefox, you click on the gear icon in the top right of the console, then check "Show Timestamps".

Though you probably want the per-tab Console there (ctrl-shift-k). ctrl-shift-j would give you the multiprocess browser console, which is very noisy.

Not gonna lie, read this hoping to pat myself on the back for being a pro, twist: learned some cool stuff, console.memory()? Neato!
Me too. `console.table` is totally new to me as well.
Doesn't work in Firefox, unfortunately. :sadface:
Yeah, also it's console.memory not console.memory()
I use the console.dir method occasionally to get the public methods and properties of HTML elements, for instance console.dir(document.body) would output all the methods and properties

    %s – string
    %i or %d – integer
    %o or %O – object
    %f – float
Why were these ever specific types, instead of just one option that looks at the parameter type?
Probably as a remnant of C's printf? Python has the same for its string formatting.
I could see both, but things like integer vs float couldn't be determined from the type, since you just have the single `number` type. That being said, you could just convert to an int on the value you're passing in rather than having the interpolation do it for you. Same thing with object vs string, you could pass in `obj.toString()` instead of just `obj` with an `%s`
Let me save someone a few minutes of confusion: generally I use console.table instead of console.dir ever since I discovered console.dir is basically unpredictable. Try using it on an Error or anything that inherits from Error and you'll see it puts out what looks like an expandable stack trace. I have no idea how or why it's implemented to do that, but basically it just varies from one object to the next and I dislike that.
Hey, this is pretty neat! Never knew you could assert with it also. This'll come in handy!
Another tip I found very useful lately

You can use `$("selector")`, even without jQuery, in console. (not sure if with firefox, works with safari and chrome)

And also `$x("path")` for xpath.

But note, this works only in console, it’s not available from javascript.

Yep, it's an alias for document.querySelector. $$ aliases document.querySelectorAll, too.
Looks like Firefox gives you `document.getElementById` with `$`. You need $$("selector") for `document.querySelectorAll`. Or rather, it looks like it does something like `[...document.querySelectorAll(arg)]` (it returns an `Array` not a `NodeList`.)
You can also use $0 to get the currently selected element in the inspector.
Wow there are some cool tricks here!

In Firefox any objects you pass to console.log are expandable, so you can say console.log("my hash", h). It seems to behave the same when you say console.log("my hash %o", h).

But there is a tricky thing that has really confused me in some debugging efforts: when expanded, the object display is "live", so it always shows the current properties of the object, not the properties as they were when you printed them. But the unexpanded view shows them as they were. So for example:

    h = {foo: "bar"}
    console.log(h)
    ▶ Object { foo: "bar" }
    h.foo = "BAR"
Then you click the triangle and you see:

    ▼ {..}
    |   foo: "BAR"
    | ▶ <prototype>: Object { .. }
I don't know if that's a bug or desired behavior, but watch out for it! In the past I've used console.log(JSON.stringify(h)) to capture the full object as-is. I guess turning it back into an object would be even nicer, so you could have a deep copy to navigate.
That's an anti-feature. When you log something in traditional sense you are recording something at that time.
It's more that the way that the console reflects how the language works, and the console is not a log, it's a REPL (so console.output would have been a nicer name than console.log).

I think it would be more confusing if the console did not work like the rest of the language does.

> It's more that the way that the console reflects how the language works, and the console is not a log, it's a REPL (so console.output would have been a nicer name than console.log).

That is entirely irrelevant. The primary purpose of `console.log` is and has always be to generate output from normal, non-interactive programs.

And it's completely wrong, `console.log` was absolutely intended as a logging method, as evidenced by its siblings `debug`, `info`, `warn` and `error`, pretty much like every logging API out there.

It's also ahistorical revisionism "the console" was added very late into the history of the language, it and the entire console API were added by Firebug in the mid aughts. The language had been a thing for a decade at that point.

> I think it would be more confusing if the console did not work like the rest of the language does.

It would be the exact opposite. When I try to output something, my intent is to show the state of that thing at that point. That JS consoles are lazy (and even deferred) has systematically been a pain point and a pain in the ass leading to eager deep cloning to ensure I can see what I actually have on hand at that point, especially in mutation-heavy code.

I'm absolutely certain the number of times I've considered the behaviour a feature rather than an annoyance is 0.

What I expect to happen when I do `console.log(obj)` is that it called `obj.toString()` which means I'd expect it to print `[object Object]` and that if I wanted to see all the values I'd have to either serialize the object `console.log(JSON.stringify(obj))` or manually generate a string `console.log(`field1: ${obj.field1}, field2: ${obj.field2}`);

The fact that the browser provides me this convenience of a link to an expandable live object is a bonus feature. I'm glad it doesn't try to deep copy the object. If it did it would make console.log useless because of the performance overhead.

If you want to capture all the fields then `console.log({...obj})` would work. But of course any of those fields that are references to objects will be live. I wouldn't expect any thing else. A print function shouldn't be required to figure out if your deep references are circular which would be required if you wanted deep copies.

It's perhaps badly named, but it's also the only way it could work in practice. A general facility for expanding any logged object to arbitrary depths would be prohibitively expensive to implement with static snapshots.

I've posted about it in more detail here: https://news.ycombinator.com/item?id=26785429.

Rather than using

  %s, %i, %o, %f, etc.
You can use a templated string, like this:

  `This is a string that has been printed ${someVar} times.`
That works great for strings / numbers, not so much for objects (unless you really want to see "[object Object]" in your debug strings).
I've put over 200 tips like that on my website: https://umaar.com/dev-tips/

Each tip has a textual explanation, and an animated gif if you're a visual learner (I know, I need to scrap gifs and move to regular videos).

There's a lot of tricks there which can hopefully improve your development and debugging workflows. Let me know if there are specific things you'd like to see. A few people have asked for how to find memory leaks.

Amazing! Thanks so much for this.
(comment deleted)
Speaking of memory leaks, I once had to debug a memory leak caused by console log entries. Objects that are logged to the console are prevented from being garbage collected, even if the console was never opened. That includes DOM nodes or I think also handles to WebGL textures.
That’s interesting. I’m guessing console.log appends to the window object somewhere, which is a fairly common method of image preloading.
Interesting, I think that's because logged objects are "live", so you can inspect their current state, not just the state at the point of logging.
Great thing! Do you support rss feeds, so that I can stay up-to-date?
(comment deleted)
If you select an item with the inspector (Chrome or Firefox), you can now refer to that HTML element as $0 in the console.
Another little trick; instead of doing:

  console.log("some label: " + JSON.stringify(someObj))
pass it as a separate parameter:

  console.log("some label: ", someObj)
and you'll get interactive expansions/manipulation in the console
No, this is totally different. In the second version, if someObj changes after it was logged, when you'll expand it you'll see the updated value. JSON.stringify freezes the value. To get the same as the first example, but interactive, you have to do:

  console.log("some label: " + JSON.parse(JSON.stringify(someObj)))
> you have to do

Technically you'd have to do

  console.log("some label: ", JSON.parse(JSON.stringify(someObj)))
> In the second version, if someObj changes after it was logged, when you'll expand it you'll see the updated value

Yes, this is something to be aware of (and is getting beaten to death throughout this comments section), but if like me you mostly use plain objects in an immutable way, you generally don't have to bother with cloning. Just keep this in the back of your head and know when it won't do what you want in a particular context.

> Just keep this in the back of your head

I would never debug with that mindstate. I don't trust myself.

It's only different if you don't treat objects as immutable.
My whole personal site[1] is one big console.log(), right down to theme matching :D Unfortunately I'm not sure anyone has actually noticed.

1: https://itsokayitsofficial.io/

I really like how that plays out as a sort of text only alternative to a site, very cool ;)
Hey thanks! I love the sense of discovery the dev console provides. When you go to someone's site and hit `F12` to see what's going on and BOOM, your confronted with a message, like they were waiting for you personally. It's those little bits of hidden magic the internet can provide.
I just see a tree and a name, is that all it is? Not sure what you meant but I’m on my phone so I can’t really look at the console.
Go to the desktop version and open the dev tools console :)
No output in Safari for some reason. I see it in Chrome.
oh really? Interesting, why wouldn't Safari support the console APIs? Huh, well, I'm going to take the high road and say Safari intentionally not supported for such-and-such high-minded reasons.
Fun

But you should proofread it for typos.

Thanks! My IDE is a terrible spell-checker
Clever and clean, Nice work!
Much appreciated!
I didn't know about .memory, .trace() and .assert(), all of which are very helpful. Up till now, I've had to add a try-throw-catch to get a stack trace and be able to follow synchronous execution flow leading up to a given function call, but console.trace will do that for me, so yay.
(comment deleted)
for what its worth i have this guy blocked on twitter - all he does is repost basic APIs you can get from MDN. classic grift playbook.
Not everybody have already learned everything. Sure, documentation is a really good source of truth, but for some people simpler introduction than a whole whitepaper's worth of information, which you get in MDN, might be more digestible.

I don't think closing on people just starting to learn is a good way to expand the field of software engineering.