DEV Community

Cover image for How to properly log objects in JavaScript?
Arek Nawo
Arek Nawo

Posted on • Originally published at areknawo.com

How to properly log objects in JavaScript?

I've talked about the Console API in one of my previous articles. However, there was one especially important detail that I didn't cover back then - the problem of logging objects. So, what's the problem and how to solve it properly?

What's the problem?

You've probably logged an object to the console before, right?

const obj = { key: "value" };

console.log(obj);
Enter fullscreen mode Exit fullscreen mode

Now, I don't want to remind you that everything in JS is an object. It's not important here. We're interested in properly logging an object, which here is just a "simple" key-value structure.

Above you can see the most basic way to log an object - by using console.log(). Don't get me wrong - it's completely fine to do it that way, but it has one fundamental flaw - dynamic evaluation.

Dynamic evaluation

When you get into your console window, you'll see your object logged nicely in a form of an expandable tree. There will also be a little preview of what you can find inside.

Example console.log() output

But, while the preview itself shows the values (or rather a small fraction of them) from the moment of calling the console.log(), the tree that you have to expand manually doesn't follow the same rule.

const obj = { key: "value" };

console.log(obj);
setTimeout(() => {
  obj.key = "changed";
}, 2000);
Enter fullscreen mode Exit fullscreen mode

With the snippet above, unless you manage to expand your logged object within 2 seconds, the value of key property in the console will be equal to "changed". That's due to the value being dynamically-evaluated at the moment of expanding the tree. However, from that point on, even if you change the value in your code, the logs will remain the same.

Console.log() dynamic evaluation

This whole thing might not be of a concert to you. But, if you work on code where the current state of the object's properties is important, you might want for your logs to be a bit more representative of the moment they were called in.

Copying

The most obvious solution to such a problem would be to simply copy the logged object. Sure, it can take a bit of additional memory, but at the debugging phase, it's not that important.

Browsers implementing the ECMAScript 6 (ES6) standard, have a method called Object.assign() which is exactly what we need:

// ...
console.log(Object.assign({}, obj));
Enter fullscreen mode Exit fullscreen mode

Object.assign() assigned all the properties of the passed objects to the first one and then returns it. This makes for an effective one-liner, in which we copy all the properties (even from multiple objects) to a single target object, that's then displayed. In this way, we make sure that our logs won't be altered in the future.

Another, even better solution is the spread syntax (...) which has a bit worse cross-browser support, but does essentially the same thing with less code to write:

// ...
console.log({...obj});
Enter fullscreen mode Exit fullscreen mode

Here, we expand/spread/copy properties from object obj to the new object literal the operator is used within.

Deep copy

Now, if you work only with single-dimensional aka "flat" objects, you need to look no further - the solution above should satisfy all your needs.

However, because objects are copied by reference instead of value, neither spread syntax nor Object.assign() will work with nested objects. Sure, top-most values will be fine, but all the properties of the nested objects will still be determined dynamically (i.e. after you expand them).

To solve this problem using a technique similar to what we've done a moment earlier, we'll need to use deep copying. Basically, we have to go through all the properties and copy objects explicitly when needed.

Keep in mind that we also have to consider cases like circular references and other copied-by-reference values like arrays (depending on our needs). Thus, it's easier to simply use a utility library like Lodash, instead of implementing the entire functionality on your own.

// ...
console.log(_.cloneDeep(obj));
Enter fullscreen mode Exit fullscreen mode

Here, we're using the cloneDeep() method from Lodash to deeply copy/clone the desired object.

Remember that if you don't want to import or even npm install the entire library, you can always use the method on its own through the extracted NPM package.

JSON

Copying an object is a great option when wanting to maintain the nice tree formatting and all that fancy stuff. But if all you need is some basic information, JSON.stringify() might be a good alternative.

// ...
console.log(JSON.stringify(obj, null, 1));
Enter fullscreen mode Exit fullscreen mode

You might not know that JSON.stringify() accepts 2 optional arguments. I've talked about this already in one of my "Tricks" articles. The first one is a replacer that can alter the processed values, while the second one is used as a number of spaces to insert within the created string. This way we end up with something like this:

JSON.stringify() formatted output

Circular references

Now, while JSON.stringify() can deal with usual nested objects and even arrays just fine, it really struggles with circuital references, i.e.:

const obj = {key: "value"};
obj.reference = obj;
Enter fullscreen mode Exit fullscreen mode

There's an easy way to get around that - the replacer function. Take a look:

// ...
const log = value => {
  const cache = [];

  console.log(JSON.stringify(value, (key, value) => {
      if (typeof value === "object" && value !== null) {
        if (cache.indexOf(value) !== -1) {
          return "[[circular]]";
        }
        cache.push(value);
      }
      return value;
  }, 1));
};

log(obj);
Enter fullscreen mode Exit fullscreen mode

What we've got here is essentially cloneDeep()-like edge-case handling, but for JSON.stringify(). Instead of displaying the actual value, we show the "[[circular]]" string, to notify about the presence of a circular reference.

Handling JSON.stringify() with circular references

If you want, then with a bit of additional code, you could also implement the full support for displaying circular references.

Again, copying an object might be a better option most of the time, because of all the additional formatting and easy-to-use Lodash cloneDeep() method. But, ultimately, I think it's just a matter of preference.

Bottom line

It seems like even simple things like console logging can sometimes get quite complicated. Well, I guess it's in the very nature of the programming profession. Anyway, I hope you find this article useful, and that it'll help you get even better at the art of debugging!

If you like the post consider sharing it and following me on Twitter or Facebook. If you're interested, I also recommend checking out my YouTube channel. Again, thanks for reading this piece and have a nice day!

Buy Me A Coffee

Top comments (1)

Collapse
 
easilybaffled profile image
Danny Michaelis

@areknawo , great post! I get bit by circular references all the time.
I can't see a post about console.log and not share console.tap. It's a small upgrade for console, made to solve the fact that console.log returns undefined. Instead tap returns the first value you passed in.
console.tap = (v, ...args) => ( console.log(v, ...args), v).
Imagine trying to log this function:

const recompose = ({
    a
    b,
    …rest
}) => ({
    [a]: b,
    details: rest
})

So if you're writing your own logging, why not be nice to yourself and return the object?