Limitations and considerations when wrapping console.log

Posted on


JavaScript and console.log

If you've tried to do this sort of thing before, you'll understand that it's a pain to keep the correct line number and source mapping in the output if you are using a higher-order function or closure. This problem arises when you'd like to run conditionals or hooks before console.log executes.

If you don't know what I mean - that's okay - just know that the function needs to be called at the actual line number you want. In short, you cannot mess with any of the parameters before they enter console.log. The function needs to be called directly.

Here's a short example:

JS
function log(message, ...args) {
  console.log("I am messing with console.log...", message, ...args);
}

log("hello!", {}); // logs with line number = 2, in function log

The output will be as you expect, but it will show the line number from where it is called, not where log("hello!", {}) is called.

If we want the line number, we have to do something like this:

JS
var log = console.log;

log("hello!", {}); // logs with line number = 3, where we want it

There is another route we can take if we desire to mess with the logging parameters yet keep the line number, but it's a bit verbose. It is the best (and probably only) approach I have found that enables this sort of capability.

JS
function log(message, ...args) {
  return Function.prototype.bind.apply(console.log, console, ["hi!", message, ...args]);
}

log("hello!", {})(); // logs "hi!" and "hello!" with line number = 4, where we want it
                     // NOTE THE EXTRA PARENTHESES!

Essentially, we bind the function console.log to the console object (to make sure this = console) and return a pre-baked function with the desired parameters. However, we do have to add an extra set of parentheses to make sure that returned function is called.

Which Approach To Use

So, we've gone over the limitations of console.log and ways around that. If you desire to just turn off logging based on a global parameter, you'd be fine with the first approach, like so:

JS
const debug = true;
const log = Function.prototype; // no-op function by default
if (debug && window.console) {
  log = window.console.log;
}

log("hello!"); // works if debug == true, else does nothing

If you want to add conditionals, format the output, or add hooks that execute before the log (like logging to a remote server) then you'll need the second approach. Here's a quick example:

JS
const debug = true;
const log = Function.prototype; // no-op function by default
if (debug && window.console) {
  log = window.console.log;
}

function log(debug, hook, format) {
  return (message, ...args) => {
    if (debug) {
      hook(message, args); // call our hook
      return Function.prototype.bind.apply(console.log, console, format(message, args)); 
      // return our binded function with a formatted output
    }
    return Function.prototype; // else return no-op function!
  }
}


log("hello!")(); // works if debug == true, else does nothing

This is a powerful approach, albeit a bit verbose. You can add pretty much anything you need to execute as long as you return the bound, applicated function at the end. You could use this to "turn off" parts of your application, resolve stack traces to a remote server, etc.

Keep in mind the no-op function has minimal to no performance impact (see here for a great article on the subject). Therefore, you could leave all your logging statements in production code, although it's best to (a) leave a cleanup script that will remove these statements or (b) make sure you redefine the console with no-op functions when building to production.

Thanks for reading!

© Copyright 2024 Aiden Stern