DEV Community

Cover image for How to Correctly Wrap a JavaScript Function
Todd H. Gardner for TrackJS

Posted on • Originally published at trackjs.com on

How to Correctly Wrap a JavaScript Function

Wrapping JavaScript functions lets you add common logic to functions you do not control, like native and external functions. Many JavaScript libraries, like the TrackJS agents, need to wrap external functions to do their work. Adding wrappers allows us to listen for Telemetry, errors, and logs in your code, without you needing to call our API explicitly.

You may want to wrap a function to add instrumentation or temporary debugging logic. You can also change the behavior of an external library without needing to modify the source.

Basic Function Wrapping

Because JavaScript is wonderfully dynamic, we can wrap a function by simply redefining the function with something new. For example, consider this wrapping of myFunction:

var originalFunction = myFunction;
window.myFunction = function() { 
  console.log("myFunction is being called!"); 
  originalFunction(); 
}
Enter fullscreen mode Exit fullscreen mode

In this trivial example, we wrapped the original myFunction and added a logging message. But there is a lot of things we didn’t handle:

  • How do we pass function arguments through?
  • How do we maintain scope (the value of this)?
  • How do we get the return value?
  • What if an error happens?

To handle these things, we need to get a little more clever in our wrapping.

var originalFunction = myFunction;
window.myFunction = function(a, b, c) { 
  /* work before the function is called */ 
  try { 
    var returnValue = originalFunction.call(this, a, b, c);
    /* work after the function is called */ 
    return returnValue; 
  } catch (e) { 
    /* work in case there is an error */ 
    throw e; 
  } 
}
Enter fullscreen mode Exit fullscreen mode

Notice that we are not just invoking the function in this example, but call-ing it with the value for this and arguments a, b, and c. The value of this will be passed through from wherever you attach your wrapped function, Window in this example.

We also surrounded the whole function in a try/catch block so that we can invoke custom logic in the case of an error, rethrow it, or return a default value.

Advanced Function Wrapping

The basic wrapping example will work 90% of the time, but if you are building shared libraries, like the TrackJS agents, that’s not good enough! To wrap our functions like a pro, there are some edge cases that we should deal with:

  • What about undeclared or unknown arguments?
  • How do we match the function signature?
  • How do we mirror function properties?
var originalFunction = myFunction;
window.myFunction = function myFunction(a, b, c) { /* #1 */
  /* work before the function is called */
  try {
    var returnValue = originalFunction.apply(this, arguments); /* #2 */
    /* work after the function is called */
    return returnValue;
  }
  catch (e) {
    /* work in case there is an error */
    throw e;
  }
}
for(var prop in originalFunction) { /* #3 */
  if (originalFunction.hasOwnProperty(prop)) {
    window.myFunction[prop] = originalFunction[prop];
  }
}
Enter fullscreen mode Exit fullscreen mode

There are 3 subtle but important changes. First (#1), we named the function. It seems redundant, but user code can check the value of function.name, so it’s important to maintain the name when wrapping.

The second change (#2) is in how we called the wrapped function, using apply instead of call. This allows us to pass through an arguments object, which is an array-like object of all the arguments passed to the function at runtime. This allows us to support functions that may have undefined or variable number of arguments.

With apply, we don’t need the arguments a, b, and c defined in the function signature. But by continuing to declare the same arguments as the original function, we maintain the function’s arity. That is, Function.length returns the number of arguments defined in the signature, and this will mirror the original function.

The final change (#3) copies any user-specified properties from the original function onto our wrapping.

Limitations

This wrapping is thorough, but there are always limitations in JavaScript. Specifically, it’s difficult to correctly wrap a function with a non-standard prototype, such as an object constructor. This is a use case better solved by inheritance.

In general, changing the prototype of a function is possible, but it’s not a good idea. There are serious performance implications and unintended side-effects in manipulating prototypes.

Respect the Environment

Function wrapping gives you a lot of power to instrument and manipulate the JavaScript environment. You have a responsibility to wield that power wisely. If you’re building function wrappers, be sure to respect the user and the environment you’re operating in. There may be other wrappers in place, other listeners attached for events, and expectations on function APIs. Tread lightly and don’t break external code.

JavaScript breaks a lot, and in unpredictable ways. TrackJS captures client-side JavaScript errors so you can see and respond to errors. Give it a try free, and see how awesome our function wrapping is.

Top comments (0)