For the next few weeks we will be exploring the internal details of our JavaScript SDK in a series of posts. Our hope is that this will help developers debug any issues with our SDK and give us an opportunity to outline some cool best practices for JavaScript libraries. In this post, we’re going to drill into how error handling works in our SDK.
When writing library code targeted at running on third party sites, the only truly known thing is the code that you provide – everything else is an unknown and potentially unsafe factor:
- The browser executing the code
- The JS environment, possibly with augmented prototypes or shadowed constructors
- Invalid arguments passed to your code
- Functions/callbacks invoked by your code having unexpected side effects or errors
Because of this, even “perfect” code will in many cases end up misbehaving or throwing errors. So, we’ve instrumented our JavaScript SDK with code for detecting and reporting such errors back to us for further investigation.
To demonstrate how we do this, we’re going to walk through a simple scenario: We’ll create a small library and then instrument it just like we’ve instrumented the JS SDK. To start off, let’s create an example that illustrates how many libraries set up their public APIs:
var mylib = (function () { var mylib = { // this will naively create new properties on our mylib object provide: function (name, fn) { this[name] = fn; } }; .. // create mylib.foo mylib.provide('foo', function () { .. }); // create mylib.bar mylib.provide('bar', function () { .. }); return mylib; })();
The conventional way to add library-wide error handling to this code is something like the following:
function guard(fn) { return function () { try { return fn.apply(this, arguments); } catch (e) { // log error .. // re-throw to halt execution throw e; } }; } var mylib = { provide: function (name, fn) { // use 'guard' to wrap the function in the error handler this[name] = guard(fn); } };
In addition to the public interface, we instrument all entry points — including those executed via setTimeout/setInterval; event handlers such as onclick or onmessage; and callbacks used with JSONP or XMLHttpRequest. Having a guard function makes this easy. We also pass the name of the entry point to the guard function, something that allows us to log the error, the entry point that eventually led to it, and its arguments. This makes it easy for us to reproduce issues, or at least to narrow down the possible causes.
Managed errors
The above instrumentation catches all errors that are thrown when executing – but what if there are errors that we actually want to not catch, like an error due to assertions made on passed arguments? These are clearly errors that we either don’t want to catch/log, or at least want to treat differently. The solution we have chosen is to simply define our own error type that we use when throwing in a managed way, and then check for this in the catch block:
var ManagedError = function (message) { Error.prototype.constructor.apply(this, arguments); this.message = message; }; ManagedError.prototype = new Error(); function guard(fn) { return function () { try { return fn.apply(this, arguments); } catch (e) { if (e instanceof ManagedError) { // re-throw immediately throw e; } // log error .. // re-throw to halt execution throw e; } }; } .. mylib.provide('foo', function ( /*..*/ ) { .. throw new ManagedError('Invalid argument'); });
With this approach, managed errors are re-thrown to the calling code, while unmanaged ones are captured and logged.
Calling external functions
The JS SDK has an event mechanism, and many of the exposed functions accept a callback argument that the SDK will invoke at some point. But this can easily be a source of false positives if any of these contain errors, so how do we solve this? Again, the conventional way of doing this is to explicitly wrap each invocation in a try/catch block, but this can easily become unwieldy. Instead, we simply wrap all such functions when they pass through the initial guard function:
// Helper function for recursively wrapping functions function wrap(fn, value) { // not all 'function's are actually functions! if (typeof value === 'function' && /^function/.test(value.toString())) { return fn(value); } else if (typeof value === 'object' && value !== null) { for (var key in value) if (value.hasOwnProperty(key)) { value[key] = wrap(fn, value[key]); }; } return value; } function unguard(fn) { return function () { try { return fn.apply(this, arguments); } catch (e) { // surface the error setTimeout(function () { throw e; }, 0); } }; } function guard(fn) { return function () { // capture the arguments and unguard any functions var args = Array.prototype.slice.call(arguments).map(function (arg) { return wrap(unguard, arg); }); try { return fn.apply(this, args); } catch (e) { if (e instanceof ManagedError) { throw e; } // log error .. // re-throw to halt execution throw e; } }; }
Returning functions to external code
The last example that we’re going to cover here is the case where our functions actually return functions, or objects with function members. These represent an entry point to our code and we also want to guard these. Luckily, this can be done by simply wrapping the response value using the same pattern as above:
function guard(fn) { return function () { // capture the arguments and unguard any functions var args = Array.prototype.slice.call(arguments).map(function (arg) { return wrap(unguard, arg); }); try { return wrap(guard, fn.apply(this, args)); } catch (e) { if (e instanceof ManagedError) { throw e; } // log error .. // re-throw to halt execution throw e; } }; }
Differentiating between internal and external callers
Some of you might have noticed one problem with our code so far: It doesn’t differentiate between internal and external callers. After all, it’s not uncommon for library code to call into other functions of the same library, is it? As it is now, all functions will effectively be wrapped, causing both an overhead in terms of function calls and errors in the reporting, as each function in the call chain will result in the error being logged. The solution? Simply create two views of our library, one to be used internally and one to be exported:
var mylib = (function () { // object that will be exported to external callers var public = {}; // object that will be used by internal callers var mylib = { // this will naively create new properties on our mylib object provide: function (name, fn) { this[name] = fn; // use 'guard' to wrap the function in the error handler // for external use public[name] = guard(fn); } }; .. return public; })();
All callers defined inside the main closure will now be referencing the internal version of mylib, while external callers will be referencing the public version. An added side effect of this is that modifications made externally — such as someone replacing a function on the external object (so-called monkey patching) — do not have any effect on the internal code. Whether this is good or bad is entirely up to you, but from experience we have seen that monkey patches often change expected behavior and can quickly become misaligned with the rest of the code (causing more issues that they fix). We’ve also found that actively monitoring errors through code such as this reduces the need for such patches.
(Since first writing this post, the JS SDK has been migrated to use only CommonJS internally, so we no longer rely on this separation)
Summary
We are continuously working on improving the JS SDK, and one of the key properties that we strive for is for it to be able to gracefully handle any error condition, so as to not disrupt the execution of your app. Having insights into the errors that are occurring on real third-party sites are of immense value and lets us quickly react to new issues being introduced, as well as fix old, but rarely seen, ones.
If you want to see the full example created in this post in action, then you can find it in this jsFiddle example – check the developer console for the output!
Sean Kinsey is an engineer on the Platform team.