DEV Community

Danny Michaelis
Danny Michaelis

Posted on

Why (We) Make Room For `console.log`?

I have been programming for about ten years. I spent the first four pretending to learn Java in college and then started with JavaScript in 2014. That was a little before Babel, and the proposals for ES6 were introduced. I have gotten to see what JavaScript had been, the promise of what it would become, and now to see it surpass all expectation. Modern JavaScript, the evolution of the language starting with ES6, has been incredible, but it has left a pinprick of a problem. console.log does not fit with modern JS features or style.

The Ktchn Snk

Bellow is an example function that is using a slew modern JS features:

const isEqual = require('lodash.isequal'); const moment = require('moment'); function formatCurrency(num) { return ( '$' + num.toString().replace(/(\d\d\d(?=[^.]))/g, function($1) { return $1 + ','; }) ); } const transactionData = { amount: 100, date: Date.now(), description: 'Shrimp! Heaven! Now!', type: '', from: 'Daniel', to: 'Mom' }; const pickAndFormatTransaction = ( { amount, date, description } ) => ( { amount: Number( amount ) ? formatCurrency( amount ) : amount, date: moment( date ).format( 'DD/MM/YYY' ), description } ); console.log(pickAndFormatTransaction(transactionData));

❓What do you think this function would have looked like just a few years ago?

The function is concise and deliberate. By which I mean, there is no extra syntax. I don't need to use a return, which also means I don't need to declare a const resultingObject =...; to return. I dodged having to name the parameter to pull out some values. I also don't need to create an if/else block for amount. I don't mean to diminish things like return or explicitly naming values. They have their place. Instead, I relish in how sparse the function can be, while still being readable.

In my eagerness to cut down on clutter, I have created a new hurdle. The pickAndFormatTransaction is so concise that there's no room for a console.log. If something goes wrong, it's going to take extra work to debug it.

Something Goes Wrong

const isEqual = require('lodash.isequal'); const moment = require('moment'); const diff = require('jest-diff'); function formatCurrency(num) { return ( '$' + num.toString().replace(/(\d\d\d(?=[^.]))/g, function($1) { return $1 + ','; }) ); } function pickAndFormatTransaction({ amount, date, description }) { return { amount: Number(amount) ? formatCurrency(amount) : amount, date: moment(date).format('DD/MM/YYY'), description }; } const expected = { amount: '$0.00', date: '19/05/2019', description: 'Shrimp! Heaven! Now!' }; const actual = pickAndFormatTransaction({ amount: 0, date: 1558307309712, description: 'Shrimp! Heaven! Now!' }); // MDN.io/console/assert console.assert( isEqual(expected, actual), 'EXPAND FOR PRETTY DIFF: \n%s', // Using string substitutions MDN.io/console#Usage diff(expected, actual) );

Run and take cover! This will fail!

Even if you know why amount is wrong, how would you go about adding console.log to the troubled code?

const isEqual = require('lodash.isequal'); const moment = require('moment'); const diff = require('jest-diff'); function formatCurrency(num) { return ( '$' + num.toString().replace(/(\d\d\d(?=[^.]))/g, function($1) { return $1 + ','; }) ); } const pickAndFormatTransaction = ( { amount, date, description } ) => ( { amount: Number( amount ) ? formatCurrency( amount ) : amount, date: moment( date ).format( 'DD/MM/YYY' ), description } ); const expected = { amount: '$0.00', date: '19/05/2019', description: 'Shrimp! Heaven! Now!' }; const actual = pickAndFormatTransaction({ amount: 0, date: 1558307309712, description: 'Shrimp! Heaven! Now!' }); // MDN.io/console/assert console.assert( isEqual(expected, actual), 'EXPAND FOR PRETTY DIFF: \n%s', // Using string substitutions MDN.io/console#Usage diff(expected, actual) );

🛠 Break open the code and try adding console.log to see why the amount isn't coming out right.

I took a swing at it as well.

const pickAndFormatTransaction = ( {
        amount,
        date,
        description
} ) => {
    const formattedAmount = Number( amount )
            ? formatCurrency( amount ) 
            : amount;
    console.log( Number( amount ), formattedAmount );
    return {
        amount: formattedAmount,
        date: moment( date ).format( 'DD/MM/YYY' ),
        description
} };

If yours is anything like mine, it's none too pretty. I suppose “pretty” is subjective but work with me here; it'll be worth it.

console.log -> undefined

You may have noticed that when you've run the RunKit examples above, you see the console.log output and then undefined. That undefined is the result of console.log(...) ( check it out in the WHATWG spec if you like ). This is why debugging is cumbersome. We have to bend over backward and around this undefined. I have yet to see a time when I need that undefined. We could save ourselves plenty of work if console.log returned the value it was logging instead of undefined.

console.tap

I've created console.tap to be the logging function modern JS has been missing.

console.tap = v => { console.log( v ); return v; };



Oo Aah
…wait, that's it?

First, yes I'm adding it to the global console object. That is my choice, I'm a mad man. Second, the function revolves around simplicity. It takes one* value, logs that value, and returns that value. To the calling function, and the context around it, nothing happens. Which means there is no extra overhead or setup to debugging.

*In reality you can pass additional arguments into tap. They'll be logged but not returned

A side note on the name:
I got the name for console.tap from a helper function of the same name. The tap helper takes a function and a value runs the function with the value, ignores the result, and returns value. console.tap is essentially tap with console.log passed as the function. You can see examples of tap in Lodash and Ramda. As for where the original name tap came from I'm not sure, but I'd really like to know.

Using Tap

We'll return to pickAndFormatTransaction later. Instead here's something a little smaller.

function parseNumbers(num) { return Number(num) || 0; } function removeEvens(num) { return num % 2; } const result = ['1', '2', 'zero' , 3, 4, 5] .map(parseNumbers) .filter(removeEvens) .reduce(( acc, v ) => Math.max(acc, v)); console.log(result);

❓There is no bug here ( at least I don't think there is ) but where would you put the console.log if there was?

map, reduce, and filter were some of the first indications of where ES6 and modern JS were heading. When you chain them together, you get the same issue as before. There's no room to fit a console.log. You have to rip the chain apart to see what is going on in the middle of it.

const filtered = ['1', '2', 'zero' , 3, 4, 5]
    .map(parseNumbers)
    .filter(removeEvens);
console.log( filtered );

const res = filtered.reduce(( acc, v ) => Math.max(acc, v));

console.tap, on the other hand, can fit just about anywhere.

function parseNumbers(num) { return Number(num) || 0; } function removeEvens(num) { return num % 2; } const result = console.tap(['1', '2', 'zero' , 3, 4, 5] .map(parseNumbers) .filter(removeEvens)) // <- this parens closes tap .reduce(( acc, v ) => Math.max(acc, v)); console.log(result);



🛠 move around just the closing ) for console.tap to see each result

console.tap could have also been used on each part of the chain since each function produces an array.

This next example doesn't even use any modern features, and it still suffers from the same problem.

function getUserId( user ) { return user.id } const storage = { store: { user: '{“id”:1}' }, getItem( name ) { return this.store[name] } } var userID = getUserId( JSON.parse(storage.getItem( 'user' )) );

❓ Anyone else excited for the pipeline operator?

If and when JSON.parse erupts with Unexpected token o in JSON at position 1, you've got to yank out storage.getItem to realize you accidentally stored [object Object] but with tap:

function getUserId( user ) { return user.id; } const storage = { store: { user: '{“id”:1}' }, getItem( name ) { return this.store[name]; } } const userID = getUserId( JSON.parse( console.tap( storage.getItem( 'user' ) ) ) );

🛠 Move around console.tap. What do you get from console.tap(JSON).parse?

As forpickAndFormatTransaction, that overachiever, why don't you give tap a try.

The Ktchn Snk

const isEqual = require('lodash.isequal'); const moment = require('moment'); function formatCurrency(num) { return ( '$' + num.toString().replace(/(\d\d\d(?=[^.]))/g, function($1) { return $1 + ','; }) ); } const transactionData = { amount: 100, date: Date.now(), description: 'Shrimp! Heaven! Now!', type: '', from: 'Daniel', to: 'Mom' }; const pickAndFormatTransaction = ( { amount, date, description } ) => ( { amount: Number( amount ) ? formatCurrency( amount ) : amount, date: moment( date ).format( 'DD/MM/YYY' ), description } ); console.log(pickAndFormatTransaction(transactionData));

🛠 throw in a few console.taps and see what you can see.

Without adding anything but console.tap you can log the whole returning object, or to anything involved in amount and moment. For description you will still have to expand the shorthand to description: description.

Have Fun!

I have created a module for console.tap that takes care of some extra details ( like providing a polyfill, ponyfill, and babel macro) - check the docs for more ) but the function declaration is so small you can write it yourself when you need it.

console.tap = v => { console.log(v); return v; }; // or, for the bold console.tap = v => (console.log(v), v);

NPM

Special thanks to Debbie Kobrin, Stephen Smith, Justin Zelinsky, Sam Neff, and Julian Jensen for reviewing the post.

Top comments (6)

Collapse
 
jacobmparis profile image
Jacob Paris

I've always just used the comma operator for this which I can see you do in the very last line

Collapse
 
easilybaffled profile image
Danny Michaelis

I'm glad to see someone else has used the comma operator. I had started the same way, but over time, it was simpler for me to pull the functionality into its own function.
Also, it has been my experience that most people aren't familiar with the comma operator, which isn't helped by ESLint's no-sequences rules.

Collapse
 
samuelneff profile image
Samuel Neff

Most of the places I've been the comma operator is disallowed due to linting rules or SonarQube. I would use it more if not for that. eslint.org/docs/rules/no-sequences

Collapse
 
grumpytechdude profile image
Alex Sinclair

I really like this!
That said - when is this more useful than just running a debugger? I'd like to understand, but I'm currently failing to.

Collapse
 
samuelneff profile image
Samuel Neff

Running a debugger requires setting a breakpoint and inspecting the value. It's a lot more of a commitment when just adding a console statement is simpler for many situations. This is especially true when doing something like debugging a statement that might get hit a few dozen times with different args and you don't know which combination of args causes the wrong output. Debugging and inspecting each invocation will take a lot more time than logging the details. Once the details are identified, then you can set a conditional breakpoint if you still need more info.

Collapse
 
samuelneff profile image
Samuel Neff

Simple addition that simplifies a lot of situations. Nice find.