How to write bulletproof function wrappers in JavaScript
In our client JavaScript SDK – Raven.js – we make use of a lot of function wrapping. We even provide a utility API method, Raven.wrap
, that automatically wraps a function in try/catch and passes any caught errors to Raven.captureException
, Raven’s primary error reporting function.
var wrappedFunc = Raven.wrap(function () {
foo(); // ReferenceError: foo is not defined
});
wrappedFunc(); // catches error, reports to Sentry
Over the years I’ve written a lot of function wrappers. They’re handy, powerful tools that belong in the JavaScript programmer’s toolbox. And if there’s one thing I’ve learned, it’s that wrapping a function in JavaScript is trickier than it looks.
In this article, we’ll learn how function wrappers are useful, how they’re written, and how they should be written. But first…
Why use function wrappers?
A “function wrapper” is a function whose purpose is to call a second, “wrapped” function, with some minor amount of additional computation. Function wrapping is a common practice in JavaScript (and most other languages), with many practical uses:
Binding this
You're probably familiar with Function.prototype.bind
, which returns a function that, when called, has its this
keyword set to the provided value:
function whosThis() {
console.log(this);
}
whosThis.call('me'); // => 'me'
var boundWhosThis = whosThis.bind('them');
boundWhosThis(); // => 'them'
boundWhosThis.call('us'); // => 'them' (cannot override `this`)
You can think of bind
as producing a function wrapper whose purpose is to call the original function, with a small additional quirk: it permanently changes the value of this
.
We can do the same thing manually, without bind
, but instead using a closure:
var them = 'them';
var boundWhosThis = function () {
whosThis.call(them);
};
boundWhosThis(); // 'them'
boundWhosThis.call('us'); // => 'them' (cannot override `this`)
If you've spent a good amount of time writing browser-based JavaScript applications, you know how important it can be to manage the value of this
across different functional scopes. Function wrappers produced by Function.prototype.bind
are a handy, commonly-used tool.
Profiling
A function wrapper can be used to transparently record the duration of a function invocation. This can be helpful when profiling the performance of your application.
function profile(func, funcName) {
return function () {
var start = new Date(),
returnVal = func.apply(this, arguments),
end = new Date(),
duration = stop.getTime() - start.getTime();
console.log(`${funcName} took ${duration} ms to execute`);
return returnVal;
};
}
var profiledMax = profile(Math.max, 'Math.max');
profiledMax.call(Math, 1, 2);
// => "Math.max took 2 ms to execute"
Note that this example only times the duration of synchronous code. If the wrapped function triggers an asynchronous callback (e.g. via setTimeout
), any time spent in that callback will not be captured.
Mixins
Mixin patterns in JavaScript are often used to augment a prototype method by wrapping it with additional behavior.
In the example below, the makeRoyalMixin
function changes an object by wrapping that object's getName
prototype method with a function that changes its output:
function User(name) {
this.name = name;
}
User.prototype.getName = function () {
return this.name;
};
function makeRoyalMixin(klass) {
var oldGetName = klass.prototype.getName;
var designation = ordinal((Math.random() % 10) + 1); // '1st', '3rd', '9th', etc
klass.prototype.getName = function () { // the wrapper
return oldGetName.call(this) + ' the ' + designation;
};
}
var user = new User('Janey Smith');
user.getName(); // => "Janey Smith'
makeRoyalMixin(User);
user.getName(); // => "Janey Smith the 7th"
But wait, there’s more
Profiling and Mixins are just two simple examples. Here's a few other useful applications:
Code coverage – use function wrappers to detect if a function has been invoked during the execution of an application
Hiding complexity – use function wrappers to provide a simpler API than the underlying code
Cross-platform code – function wrappers are often used to smooth-out minor incompatibilities between an API on different platforms
Partial applications – use a function wrapper to fix specific arguments to that function's invocation
Safely handling errors – our very first example, using function wrappers to transparently try/catch errors
Writing bulletproof function wrappers
Okay, we now know what function wrappers are, and how they're commonly used. Now it’s time to write one. And not just any function wrapper – a wrapper that can wrap any conceivable function, invoked every which way, without breaking anything.
Use apply and arguments
Let’s say you want to wrap a known function, like XMLHttpRequest.prototype.open
. You look up the signature of open
on MDN, and learn that it has 5 arguments: method
, url
, async
, user
, and password
. You write a wrapper for this function, passing the 5 declared arguments to the original function using call
.
var origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
// rewrite URLs containing '/foo/' to '/bar/'
url = url.replace(/\/foo\//, '/bar/')
return origOpen.call(this, method, url, async, user, password);
};
But there’s a problem with this implementation. It’s subtle, but you have changed the behavior of calling open
.
Remember that open
takes an optional number of arguments. At minimum, it can accept just method
and url
. Or it can accept method
, url
, and async
. Or it can accept all 5:
var xhr = new XMLHttpRequest();
xhr.open('GET', '/example');
// or
xhr.open('GET', '/example', false); // synchronous request
// or
xhr.open('GET', '/example', true, 'zerocool', 'hacktheplanet'); // async w/ HTTP auth
It turns out that in some JavaScript engines, the native open
implementation actually inspects the number of arguments passed.
Consider this hypothetical native implementation:
// pretend native open implementation
XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
if (arguments.length <= 3) {
this._simpleRequest(method, url, async);
} else if (arguments.length === 5) {
this._httpAuthRequest(method, url, async, user, password);
}
}
In the function wrapper we wrote, we have changed the value of arguments.length
as passed to the original open
method. By doing .call(this, method, url, async, user, password)
, we have guaranteed that arguments.length
will always be 5, regardless of how many arguments were passed to the wrapper.
In the hypothetical native implementation above, this means the _simpleRequest
code path will never be reached; it always calls _httpAuthRequest
, because arguments.length
is always 5.
The solution: always pass an array of arguments to the wrapped function, matching the length of the arguments provided to the wrapper. If you need to edit one of the values, make a copy of the arguments
object, edit the affected value, and pass the copy.
var origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url) {
// copy arguments
var args = [].slice.call(arguments, 0);
// rewrite URLs containing '/foo/' to '/bar/'
args[1] = url.replace(/\/foo\//, '/bar/')
// arguments.length will always be same
return origOpen.apply(this, args);
};
Note that in the example above, we’ve changed from declaring 5 parameters (method, url, async, user, password) to just 2 (method and url). Why?
Preserve arity
Like arrays and strings, function objects actually have a length
property. This property reports the arity of a function – the number of declared parameters in its function signature.
For example, in the code sample below, foo
function has an arity of 3, and bar
has an arity of 0 (no formal parameters):
function foo (a, b, c) {
}
foo.length // => 3
function bar () {
}
bar.length // => 0
Back to XMLHttpRequest.prototype.open
. Despite the documented number of variables being 5, the reported arity via the length
property is actually 2:
origOpen.length // => 2 (the original open function)
XMLHttpRequest.prototype.open.length // => 2 (our wrapper)
You might be wondering – why does this matter? Well, just as we saw earlier that some code branches differently because of arguments.length
, there is code out there that branches differently depending on the number of parameters declared in a function. If, in wrapping a function, we change its arity, we risk changing the behavior of code that inspects that function’s length
property.
Mocha test functions and the impact of arity
Consider for a moment, Mocha, a popular JavaScript testing framework.
In Mocha, you declare a test function using the it
function, which accepts both a descriptive string (what the test does) and the test function as arguments.
it('should work as expected', function () {
assert.equals(1, 1);
});
Mocha also allows you to declare an asynchronous test function. In this version, the test function itself must declare a done
parameter, which will be passed a callback function to be invoked when the test function has finished. If an exception was thrown during the execution of the test function, the done
callback accepts an Error object as an argument.
it('should work as expected, asynchronously', function (done) {
setTimeout(function () {
try {
assert.equals(1, 0);
} catch (e) {
return done(e); // pass assertion failure to `done`
}
done(); // for some reason 1 does not equal 0, call `done` - panic and freak out
}, 1000);
});
Mocha is a pretty popular testing library, and it’s likely that many of you are familiar with its synchronous vs asynchronous test API. What you may not know, however, is that Mocha decides whether a test function is asynchronous by inspecting that test function’s length
property.
Here’s the relevant bit from [Mocha’s source code:
function Runnable(title, fn) {
this.title = title;
this.fn = fn;
this.async = fn && fn.length; // <-- RIGHT HERE
this.sync = !this.async;
this._timeout = 2000;
this._slow = 75;
this._enableTimeouts = true;
this.timedOut = false;
this._trace = new Error('done() called multiple times');
this._retries = -1;
this._currentRetry = 0;
}
Let’s say that you wrap a test function that is passed to Mocha’s test runner via it
. If in wrapping that test function, you simply pass arguments and don’t redeclare the done
variable, you will reduce its arity to 0 and Mocha will not consider the test function to be asynchronous. This will cause you serious grief as you try to figure out why the heck your test function doesn’t work anymore.
The purpose of showing you this Mocha code is to demonstrate that, yes, there is code out there that inspects the length
property of a function, and a failure to preserve arity when wrapping a function could result in broken code. So whenever you can, redeclare your wrapped variables and preserve arity.
Do I really need to do all this?
I'll admit – a lot of the examples in this blog post are rare. It's not often that you will encounter code that behaves differently depending on arguments.length
or Function.length
. When writing function wrappers that operate on your own, known functions, it is unlikely that you will encounter such behavior.
But, if you're a library author, or writing 3rd-party scripts that operate in an unknown environment, or want to really futureproof your code – it couldn't hurt to safeguard against problematic behavior by using the techniques above when writing function wrappers.
Hopefully, with the skills you've learned today, you'll know when and where to practice safe function wrapping. Good luck.
Whether you want to debug Ember, do React error tracking, or handle an obscure Angular exception, we'll be working hard to provide the best possible experience for you and your team with Sentry!