Back to Blog Home

How to write bulletproof function wrappers in JavaScript

Ben Vinegar image

Ben Vinegar -

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!

Share

Share on Twitter
Share on Facebook
Share on HackerNews
Share on LinkedIn

Published

Sentry Sign Up CTA

Code breaks, fix it faster

Sign up for Sentry and monitor your application in minutes.

Try Sentry Free

Topics

Sentry

New product releases and exclusive demos

Listen to the Syntax Podcast

Of course we sponsor a developer podcast. Check it out on your favorite listening platform.

Listen To Syntax
    TwitterGitHubDribbbleLinkedinDiscord
© 2024 • Sentry is a registered Trademark of Functional Software, Inc.