Demystifying Ember Error Handling

By: Brandon Blaylock
Door in books

Ember comes with a terrific baked-in mechanism for dealing with errors. All we as developers have to do is make a few decisions about how best to use Ember’s tooling. The ease of error handling in Ember rests on Ember’s conventions and the power rests on Ember’s flexibility which allows us to customize error responses to fit our needs. Let’s first look at using Ember’s error substates to handle errors, then we will look at gaining more control over our response to errors by overriding the route’s error action.

The Default Behavior

Ember’s error substates are - by default - not a catchall error handling mechanism. So, what triggers an error in Ember? Failures in the async transitions between routes. This means the only thing that triggers these errors is a failed promise or error returned by one of the three model hooks. This has implications for the type of data that we should return from our model hooks. For example, we should be careful about returning the results of a peekRecord from the model hook. The model hooks expect to return a promise. As peekRecord returns either the record or null, the record’s absence will not trigger a route error. You can imagine a situation where a user navigates to a route with a dynamic segment referencing a missing record. The model hook will return null. There will be no route error. There will, however, be missing data and your app will likely break.

By default, when one of the async hooks in a route returns a rejected promise, it sends an action. This action bubbles up the route hierarchy. If the action is uncaught, Ember’s default error handling function fires. This function looks for specially named error templates in your application. You should name your error template the same as your route’s template, but add -error to the end of the name. The default function finds the correct error template in the current route’s tree and uses the router’s intermediateTransitionTo function to load the appropriate substate.

An example of a template tree looks like this:

  • application.hbs
  • error.hbs
  • route1.hbs
  • route1/

    • child.hbs
    • child-error.hbs
    • other-child.hbs
  • route1-error.hbs

In this case, an error on the application route will load the error.hbs template. An error on the route1/child route will load route1/child-error.hbs. An error on the route1/other-child route will render the route1-error.hbs template.

Custom Actions for Errors

To this point we have been looking at how Ember’s default error substates work. However, often we want to handle errors ourselves. For example, if we get a 401, it might make sense to redirect the user to an unauthorized or login route. To do this, we need to intercept the bubbling error action by writing our own action in the top most route that we want affected.

The error action receives two arguments that we are interested in, the error and the transition objects. The details of the error action are discussed in the ember guides so there is little reason to go too deeply into it here.

I will add that you can complete any redirect work here using the transition object. If, for example, you are redirecting to a login route after a receiving 401, you can use the transition object to store the original destination and retry the transition after authentication completes.

It is important to note that error action bubbling works exactly the same as any other action bubbling. When we implement a custom error action in our route, all error bubbling will stop at that action. This means we should handle all intermediateTransitionTo’s, or redirects in our action.

If we have a route named my-route, we can write a custom error action like this:

// in app/routes/my-route.js:

import Route from "@ember/routing/route";
import { reject } from "rsvp";

export default Route.extend({
  model() {
     return reject('rejected promise');
  },

  afterModel() {
    // This hook will not fire.
  },

  actions: {
    error(error, transition) {
      transition.send('setFlashMessage', error);
    }
  }
});

You can see this code in action here. The error returned from the model hook in this route will trigger our error action. In our error action, we are using the transition object to send another action bubbling up our route tree. This works fine, but if you look at the twiddle, you will notice that the error template does not render.

If we want to maintain the default error template rendering, or bubble our error to a parent route, we have to return true in the error action.

//app/routes/my-route.js:

import Route from '@ember/routing/route';
import { reject } from 'rsvp';

export default Route.extend({
  model() {
     return reject('rejected promise');
  },

  afterModel() {
    // This hook will not fire.
  },

  actions: {
    error(error, transition) {
      transition.send('setFlashMessage', error);
      return true;
    }
  }
});

Communicating About and Recovering from Errors

Once an error is created, the transition into the route is paused. If we have opted to use Ember’s error substate, the error returned in the model hook will be available on the error template as its model property. This has obvious benefits for communicating problems to your users.

If you are using a custom error action, the transition object passed to the error action is useful. The transition object has functions to abort the current transition, retry the transition, or send actions up to parent routes.

Conclusion

That is about it. As always, there is a lot of great information on errors in the Ember Guides and the API docs. Errors in Ember are pretty simple. They behave like bubbling actions which, if not intercepted, render default error templates. But, they can sometimes feel a bit surprising or even confusing. I hope this helps you navigate error handling in Ember and makes the whole thing feel a little less like magic.