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:
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
Even if you know why amount
is wrong, how would you go about adding console.log
to the troubled code?
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.
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.
tap
. They'll be logged but not returnedA side note on the name:
I got the name forconsole.tap
from a helper function of the same name. Thetap
helper takes a function and a value runs the function with the value, ignores the result, and returns value.console.tap
is essentiallytap
withconsole.log
passed as the function. You can see examples of tap in Lodash and Ramda. As for where the original nametap
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.
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.
)
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.
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
:
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
console.tap
s 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.
Special thanks to Debbie Kobrin, Stephen Smith, Justin Zelinsky, Sam Neff, and Julian Jensen for reviewing the post.
Top comments (6)
I've always just used the comma operator for this which I can see you do in the very last line
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.
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
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.
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.
Simple addition that simplifies a lot of situations. Nice find.