5 Ways to Reduce Javascript Errors In EmberJS

When Later was first combining all of our critical front-end features into EmberJS, our biggest initial concern was error reporting. Previously, our front-end features may have been spread across EmberJS, BackboneJS, and HAML, but we knew they were solid, and we needed error reporting to be confident that our new EmberJS features were working in production. With our Rails and iPhone projects, there were very well-respected error trackers already integrated. Our concern with Javascript is that it’s just so… noisy, with respect to errors.

When we integrated Raygun into our EmberJS app, we found we were correct in our concerns and there were thousands of Javascript errors per day. Most didn’t affect the user experience, but their high frequency obfuscated less common failures that did affect it.

In tracking down some of our Javascript errors, we found a few patterns of error types in our EmberJS code. Here are five things you can do to prevent common EmberJS errors:

1. Check isDestroyed for Component Callbacks

Users will navigate around pages quicker than you think. Frequently, a component will make some kind of an asynchronous call and by the time the call returns the component may have been destroyed. It’s simple enough to check for this using an Ember component’s isDestroyed property. Anywhere there is an element referenced in an asynchronous callback in a component, you should check if the component has been destroyed. Otherwise the callback might reference a DOM element which has been removed, causing an error.

Bad

var self = this;
model.save().then(function(){
  var commentList = self.$(".eDM--modalBody");
  var height = commentList[0].scrollHeight;
  commentList.scrollTop(height);
});

Better

var self = this;
model.save().then( function(){
  if (!self.get('isDestroyed')) {
    var commentList = self.$(".eDM--modalBody");
    var height = commentList[0].scrollHeight;
    commentList.scrollTop(height);
  }
});

2. Take Down the Things Set Up in didInsertElement, in willDestroyElement

Similar to 1 above, Ember components have a willDestroyElement event for a reason. Any time you invoke a Javascript library in a component, you also need to clean it up; otherwise, the library will continue being in memory and making calls. Pretty much any jQuery-based library will have a destroy method you should make use of. Otherwise, it may leave listeners on your DOM which can generate errors at a massive rate.

didInsertElement: function() {
  this.$('#network_image').Jcrop({
    bgColor: 'black',
    bgOpacity: 0.4
  });
},

willDestroyElement: function() {
  this.$('#network_image').data('Jcrop').destroy();
}

3. Prevent Destroying an Already Destroyed Model

Developers have a nasty habit of not using their software in a way most users would. Most notable from our UX testing is that developers only tap a button once before waiting for an action to complete, whereas users like to think that more taps to an interface will speed it up. On a delete call, this manifests as the all-too-common Attempted to handle event 'deleteRecord' on while in state root.deleted.saved Ember error.

Pretty much any button that lets a user destroy a model will need to be disabled while the model is in transit to the backend API. Fortunately, Ember Data models have a very useful helper property for this: isSaving. This property is true while the record is in a saving state; it is also true while the model is being destroyed. Simply disabling a delete button for a model while it’s in a saving state will reduce errors caused by overzealous clicking.

Bad

<button {{ action 'destroyModel' model}}>
  <i class="i-trash"></i>
</button>

Better

<button disabled={{model.isSaving}} {{ action 'destroyModel' model}}>
  <i class="i-trash"></i>
</button>

4. Disable Going “Back” to Routes with Deleted Models

If you ever delete a record and then transitionTo a parent route on completion, you’ll find that users will often hit “back” and end up going to the route with the model ID that you just destroyed, causing a 404 error. A simple way to fix this is to just use the replaceWith helper instead of the transitionTo, so the deleted route is removed from the browser history.

Bad

var self = this;
model.destroyRecord().then(function() {
  self.transitionTo('app.schedule');
});

Better

var self = this;
model.destroyRecord().then(function() {
  self.replaceWith('app.schedule');
});

5. Sync Models with WebSockets

In many cases, a user might have multiple sessions open of your app – either because they have multiple tabs open or because multiple users are interacting with the same data. Later also has native mobile apps where users may upload images and schedule content. Syncing everything across these sessions sometimes proves to be a challenge: a user might delete a model object on a mobile device; then, when they see the same object on their desktop, they may delete it again and get frustrated when this action returns a 404 error and doesn’t remove the object from view. This also generates a Javascript error every time.

While there are many options for sending websocket requests to update web pages, at Later we use Faye.js to keep models up to date. Deleted models and logouts should be sent as events to relevant users. Using a Faye service to unload models has been one of the best ways of keeping our UIs up to date and making multi-page sessions synced.

A key thing to keep in mind is that models need to be unloaded, rather than destroyed. It’s also possible that the unload action being suggested by a Faye message will arrive while the model object is in flight, as the websocket is ofter faster than the finishing DELETE HTTP action.

this.fayeClient.on('transport:up', function() {
  // the client is online
  Ember.debug('Server connection is up');
  self.updateStoreModels();
});

this.fayeClient.subscribe('/user/' + user.get('id'), this.messageHandler.bind(this));