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.
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);)
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}}`.
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.
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...
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.
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.)
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)
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
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.
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.
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.
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
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.
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`.)
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:
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.
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.
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.
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.
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:
> 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.
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.
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.
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.
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.
155 comments
[ 3.1 ms ] story [ 189 ms ] threadThen you quit web development and start a new career.
To be fair, there is also an "i" in a square, reminding me about this behaviour.
Evaluate the expression:
You get this: Expand the first arrow of the `x:`: Now evaluate: Then expand the second arrow: Now if you unexpand the arrow, you get 1, but if you expand it you get 3 =)... (Well at least in chrome)Moreover, if you expand arrow next to {value: 0}, you will see literally this:
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.
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.
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.
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.)
Doesn't help on browser, though; the options arg to console.dir isn't part of the standard there.
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
And anyway- JSON.parse(JSON.stringify(x)) would be preferable because you'd still get the browser's rich object exploration
Of course we have to assume that x and y may be objects!
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.
I'd love to see one on the step-thru debugger!
By printing out the object you get a point-in-time snapshot rather than a reference to a mutable object.
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)
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.
https://umaar.com/dev-tips/117-inspect-function-scope/
laughs in JS
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.
Wow, since when it was in JS?
https://developer.mozilla.org/en-US/docs/Web/API/Console/tab...
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:
Then you click the triangle and you see: 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.I think it would be more confusing if the console did not work like the rest of the language does.
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.
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.
I've posted about it in more detail here: https://news.ycombinator.com/item?id=26785429.
There's a more in-depth answer SO: https://stackoverflow.com/a/23392650
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.
Technically you'd have to do
> In the second version, if someObj changes after it was logged, when you'll expand it you'll see the updated valueYes, 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.
I would never debug with that mindstate. I don't trust myself.
1: https://itsokayitsofficial.io/
But you should proofread it for typos.
I don't think closing on people just starting to learn is a good way to expand the field of software engineering.