28 Sep 2015
Logging JavaScript Errors in Backbone Applications | Salt Edge

When developing rich JavaScript applications such as fentury.com, a lot of business logic runs on the client-side. In order to provide an excellent user experience, the developers must have insight into any errors that occur in application runtime. In other words, the team must have a way to be notified when an error occurs during the application execution.

The canonical way of capturing errors in an HTML+JS app is window.onerror:

window.onerror = function(message, file, line) {
  // send error to server
};

Reference: MDN page

Note however that

For historical reasons, the onerror handler has different arguments:

(as stated in the HTML spec)

That means that in some cases browsers pass 5 arguments to the onerror function instead of 3:

window.onerror = function(message, file, line, column, error) {
  // in here error.stack returns the stacktrace
};

As you can see above, the 5th argument passed to the function is an error object, which we can inspect to get the stacktrace via different techniques1. But, as the HTML spec says, we cannot rely on the error object to always be there:

function saveError(message, stack) { /* */ }

window.onerror = function(message, file, line, column, error) {
  if (typeof error == "undefined") {
    saveError(message, []);
  } else {
    saveError(message, error.stack);
  }
};

While this solution is good enough for modern browsers, it will lack any meaningful context in older ones. That is a bit of a problem since most of the time bizarre JavaScript errors happen in legacy browsers.

However, if we think about it, there’s actually a single, unified way a user can change a Backbone application state – by triggering an event that is handled with a view.

So if we can override Backbone event handler to do a try/catch before dispatching an event, we can inspect that error to deduce context. To do that, we must break open Backbone.View.prototype and replace delegateEventsfunction implementation:

_.extend(Backbone.View.prototype, {
  // We need to save the original implementation to call it later
  originalDelegateEvents: Backbone.View.prototype.delegateEvents,

  delegateEvents: function(incomingEvents) {
    var key, method,
        wrappedEvents = {},
        events        = incomingEvents || _.result(this, "events");

    // We don't have anything to do if the view has no events
    if (!events) { return this; }

    for (key in events) {
      // Ensure that eventHandler is a function before wrapping it.
      eventHandler = events[key];
      eventHandler = _.isFunction(eventHandler) ? eventHandler : this[eventHandler];

      // Take the event handling function,
      // wrap it in a try/catch block,
      // and replace it in the events declaration passed to Backbone
      wrappedEvents[key] = this.wrapEventHandler(key, eventHandler);
    }

    return this.originalDelegateEvents(wrappedEvents);
  },

  wrapEventHandler: function(event, handler) {
    return function() {
      try {
        // Call the original handler with all the arguments, and catch any errors
        // that occur
        return handler && handler.apply(this, arguments);
      } catch (error) {
        return saveError(error.message, error.stack);
      }
    };
  }
});

Given that we are catching event errors, we have also some additional context for debugging: the event that was dispatched. So we can modify our saveError function to accept additional context:

function saveError(message, stack, context) {
  if (_.isUndefined(context)) { context = {} }
  // save message, stack and context here
}

In addition, we need to change our wrapEventHandler function to pass that context to the saveError function:

_.extend(Backbone.View.prototype, {
  //...
  wrapEventHandler: function(event, handler) {
    return function() {
      try {
        return handler && handler.apply(this, arguments);
      } catch (error) {
        // Notice the 3rd argument here
        return saveError(error.message, error.stack, {event: event});
      }
    };
  }
}

If we take the code above and create a very small Backbone view with an error in the event handler, it will look like this:

<iframe><width=”100%” height=”300″ src=”https://jsfiddle.net/alisnic/zx3340xp/3/embedded/result,js,html/” allowfullscreen=”allowfullscreen” frameborder=”0″></iframe>

As you saw above, we successfully intercepted an error that occurred in the event handler. Using the same technique, we can override,Backbone.Router.prototype.route and catch errors that occur in route handlers.

Note that our approach does not exclude,window.onerror but rather complements it. There are a lot of cases when errors are triggered outside view events.

Pros:

  • in contrast with window.onerror, our approach will always have access to the native error object. We can get the stacktrace from it via different techniques.
  • by catching the errors in the different contexts, we can save additional information from them, which helps to debug the problem

Cons:

  • we break open Backbone guts and we must pay extreme attention when upgrading to not break anything2

  1. You can use error.stack or arguments.callee when any of them is available. There are libraries for that, like stacktrace.js 
  2. You can avoid prototype patching by making a base view that extends the Backbone view and overrides the desired behavior 
  • Subscribe to NEWS

    Join our mailing list to receive the latest news from Salt Edge