Skip to content
This repository was archived by the owner on Jan 22, 2026. It is now read-only.

Error Handling

Bill Heaton edited this page Jan 19, 2016 · 17 revisions

Problem

Handing data persistence operations (CRUD: create, read, update, delete) between your Ember (client) application and API server requires fault tolerance. The app needs to notify users of relevant error responses and how to recover from various error conditions, e.g. 40x, 50x errors.

Solution

Ember routes have an error substate to that is called in response to errors in the route hooks for fetching data (beforeModel, model, afterModel).

Since actions in Ember routes bubble up, the "Application" route is the top most route which can catch errors and can be utilized to handle server errors and notify users.

Depending oh how you would like to handler an error you can choose either an error substate or an action to handle the error event. Using both an action and a substate will not work.

Handing the error event with a route action

In this scenario error substates will not be used, instead the error action of an Ember.Route will be used to respond to the error.

Here is an ApplicationRoute prototype (class):

/**
  @class ApplicationRoute
  @extends Ember.Route
*/
export default Ember.Route.extend({

  actions: {
    error(resp, transition) {
      if (!resp || resp && !resp.errors) {
        Ember.Logger.error(resp);
        return;
      }
      this.onResourceError(resp);
      let codes = resp.errors.reduce(function (errorCodes, error) {
        if (errorCodes.indexOf(error.code) === -1) {
          errorCodes.push(error.code);
        }
        return errorCodes;
      }, []);
      if (codes.indexOf(404) !== -1) {
        Ember.Logger.error('404: ' + transition.intent.url);
        this.transitionTo('/not-found');
      }
    }
  },

  onResourceError(resp) {
    this.controllerFor('application').setProperties({
      'errors': resp.errors,
      'errorMessage': resp.message
    });
  }
});

Specific routes may also listen for the resourceError event which is triggered when catching an error in the updateResource() method of an adapter. In the example below you could use this.send to send the error to that route's error action handler or via the bubbling behavior to the application route's error action; or just throw resp; to re-throw the error.

/**
  @class PostRoute
  @extends Ember.Route
*/
export default Ember.Route.extend({
  initEvents: Ember.on('init', function() {
    let service = this.get('store.posts');
    if (service) {
      service.on('resourceError', this, function (resp) { this.send('error', resp); });
    }
  })
});

If an error is caught it can be re-thrown, if you want to use either the route's error action or the route's error sub-state (they only seem to work independently) you can define a route-name-error route to handle the route's error stub-state and use the setupController(controller, error) hook of the error substate which receives the error as the model. (So, you can setup the controller for the template to display those errors.)

It can get complicated fast when trying to use a combination of route actions and error substates. When you are not using a substate you need to define a route that you can use instead, e.g. 'not-found'.

Other routes in the application for a resource may have nested structures such as:

Router.map(function() {
  this.route('posts', { path: '/posts' }, function () {
    this.route('list', { path: '/' });
    this.route('detail', { path: '/:post_id' });
    this.route('edit', { path: '/:post_id/edit' });
    this.route('new');
  });
  this.route('not-found', { path: '/*path' });
});

The Ember.Route prototype for PostsRoute, PostsListRoute, PostsDetailRoute, PostsEditRoute, and PostsNewRoute to NOT need to define their own error action, instead the ApplicationRoute can handle errors. This does support throwing errors by your application code as well.

You can handle the error action wherever you choose, at the top route a general purpose handler is ideal for notifications that can be presented in the application template.

Below is the ApplicationController prototype that manages the errors and errorMessage properties and also formats server error responses for an Unprocessable Entity 422 response.

import Ember from 'ember';
const { computed, Controller } = Ember;

/**
  @class ApplicationController
  @extends Ember.Controller
*/
export default Controller.extend({

  /**
    @property {Array} errors
  */
  errors: null,

  /**
    @property {String} errorMessage
  */
  errorMessage: null,

  actions: {
    /**
      @method [actions.dismissErrorMessage]
    */
    dismissErrorMessage() {
      this.setProperties({
        'errorMessage': null,
        'errors': null
      });
    }
  },

  /**
    @property {String} unprocessableEntities - computed from errors, error code: 100
  */
  unprocessableEntities: computed('errors', function() {
    let errors = this.get('errors');
    let fields;
    if (errors && errors.length > 0) {
      // See https://github.com/cerebris/jsonapi-resources#error-codes
      errors = errors.filterBy('code', 100);
      fields = errors.map(function(error) {
        let paths = error.source.pointer.split('/');
        let attr = paths[paths.length - 1].split('_');
        attr = attr.map(function(str) {
          return Ember.String.capitalize(str);
        });
        return attr.join(' ');
      });
    }
    return (!fields) ? '' : 'Invalid fields: ' + fields.join(', ') + '.';
  })
});

And in the application template include a condition for displaying the errors:

  {{#if errorMessage}}
    <button class="error-message u-full-width" {{action 'dismissErrorMessage'}}>
      <div class='u-pull-right'>X</div>
      {{errorMessage}} - {{unprocessableEntities}}
    </button>
  {{/if}}

For the 404 Not Found template…

<h1>Page Not Found</h1>
<p>
  Please try another Url, there is nothing here :|
</p>

Handing failure using route error substates

In this scenario error actions will not be used, instead the error substate of an Ember.Route will be used to respond to the error.

The ember-jsonapi-resources addon has a example "test" app, see jr-test. This test app include example code for using error substates to handle various responses, i.e. 500, 400, 404, 422.

At the top level where all error events bubble to is the application route. An application_error substate can be added using a combination of both an application-error route and application-error template. You could just use the template if you only display the error.message and have no need to use a condition to set a title on the template.

The application error tempate below handles any error thrown by a route that is not handled by a clild route (leaf) substate.

The app/router.js file will not need any error routes added, they are built-in.

Since the model is passed to the setupController(controller, error) hook, the error.message property can be rendered to notify the user of the error.

app/templates/application-error.hbs

<h1>Oops, the app is borked…</h1>
<p>{{model.message}}</p>

Alternatively, when a substate is not used to display an error notification, your application template can have display any messages that you set on the application controller; e.g. errorMessage and errorDetails.

app/templates/application.hbs

{{#if errorMessage}}
  <button class="error-message" {{action 'dismissErrorMessage'}}>
    {{errorMessage}} {{errorDetails}}
  </button>
{{/if}}

{{outlet}}

If you want to vary the error display of the error substate template, use the setupController hook to set a title property for the template.

In a route below the application, e.g. the PostRoute an error substate can be used to branch the display of the title to differentiate between a "client" and "server" error like so:

app/templates/post-error.hbs

<h1>{{title}}</h1>
<p>{{model.message}}</p>

app/routes/post-error.js

import Ember from 'ember';

export default Ember.Route.extend({
  setupController(controller, error) {
    let title = 'Oops, this post is borked…';
    let code = error.code || error.get('code');
    if (code) {
      if (code >= 500) {
        title = 'Oops, there was a server error…';
      } else if (code === 404) {
        title = "Opps, can't find this one…";
      }
      controller.set('title', title);
    }
    this._super(controller, error);
  }
});

Since the application template may be used for errors that you do not need to transition to an error substate to handle, the user will need a way to dismiss. An action dismissErrorMessage can be used to clear application error properties.

app/routes/application-error.js

import Ember from 'ember';

export default Ember.Route.extend({
  errorMessage: null,
  errorDetails: null,

  actions: {
    dismissErrorMessage() {
      this.controllerFor('application').setProperties({
        'errorMessage': null,
        'errorDetails': null
      });
    }
  }
});

The application_error substate will be used to display any 500 errors, or a 404 error. However, in the case of a specific client error like 400 or 422 that your application should handle without making a transition, conditions will need to be added to branch the behavior between using substates and the application error template.

If you use any nested routes, for example - post/admin/edit and post/admin/create you can defined specific at that level in the route structure, (below the application's default error handing)

app/templates/admin/edit-error.hbs

<h1>{{title}}</h1>
<p>{{model.message}}</p>

Notice it's the same as the 'post-error.hbs' template above. The post error handles non-admin errors and the "/admin/" directory is specically for managing content.

  • See the jr-test route.js file for the route hierarchy of this example solution.

To assit with testing the error substates - I set the PostController of the backend (API) to respond with an error. This is the repo for the API application: blog-api using a branch 'ember-jsonapi-resources-testing'. And the commented code I used to send error responses.

Using the setupController hook to branch setting a title property as well as using a warning for a condition that should not be handled by a transition. It can be helpful during development to log the error conditions so that you can build a solution around the desired user experience.

app/routes/admin/edit-error.js

import Ember from 'ember';

export default Ember.Route.extend({
  setupController(controller, error) {
    let title = 'Oops, this post is borked…';
    let code = error.code || error.get('code');
    if (code) {
      if (code >= 500) {
        title = 'Oops, there was a server error…';
      } else if (code === 404) {
        title = "Opps, can't find this one…";
      } else if (code === 422) {
        Ember.Logger.warn('Not expecting to handle 422 in an error substate');
      }
      controller.set('title', title);
    }
    this._super(controller, error);
  }
});

The post form component sends an update action to persist changes via the Post resource endpoint.

app/templates/admin/edit.hbs

<p><strong>Edit a Blog Post</strong></p>
{{form-post post=model isNew=model.isNew on-edit=(action "update")}}

The action is triggered after the user exists the field, this prevents a flood of updates on every keystroke - from binding a model property to a form input.

app/components/form-post.js

import Ember from 'ember';
import BufferedProxy from 'ember-buffered-proxy/proxy';

export default Ember.Component.extend({
  tagName: 'form',

  resource: Ember.computed('post', function() {
    return BufferedProxy.create({ content: this.get('post') });
  }).readOnly(),

  isNew: null,
  isEditing: true,

  focusOut() {
    if (!this.get('isNew')) {
      this.get('resource').applyChanges();
      this.set('isEditing', false);
      let action = this.get('on-edit');
      if (typeof action === 'function') {
        action(this.get('post'), function callback() {
          this.set('isEditing', true);
        }.bind(this));
      }
    }
  }
  /* … */
});

The admin.edit route responds to the actions send by the form component. After the API request is made successfully the callback function sent with the action is called. Or, an error may be caught by the failed promise.

In the case of an error the changes to the model are rolled back and the error response is handled by the route. It depends on the error code whether or not a transistion will be made to an error substate. When an error is not thrown by a route model hook then a transition needs to be made explicitly via catch.

In the example below, it would be a bad experience to transition away from the admin form the user is editing due to a client error such as "Bad Request" 400, or an "Unprocessable Entity" 422. Instead error properties are set on the application controller which results in a dismissible error notification.

app/routes/admin/edit.js

The edit

import Ember from 'ember';
import ApplicationErrorsMixin from 'jr-test/mixins/application-errors';

export default Ember.Route.extend(ApplicationErrorsMixin, {
  model(params) {
    return this.store.find('posts', params.post_id);
  },

  setupController(controller, model) {
    this._super(controller, model);
    controller.set('isEditing', true);
  },

  actions: {
    update(model, callback) {
      return this.store.updateResource('posts', model)
      .finally(function() {
        if (typeof callback === 'function') {
          callback();
        }
      })
      .catch(function(error) {
        model.rollback();
        this.handleUpdateError(error);
      }.bind(this));
    }
  },

  handleUpdateError(error) {
    let doTransitionToError = true;
    if (error.code === 422 || error.code === 400) {
      doTransitionToError = false;
      this.handleApplicationError(error);
    }
    if (doTransitionToError) {
      this.intermediateTransitionTo('admin.edit_error', error);
    }
  }
});

The method handleApplicationError comes from a mixin so that both the admin/edit and admin/create routes can use the same behavior.

This mixin can is used to parse the error responses into a specific error details.

app/mixins/application-errors.js

import Ember from 'ember';

export default Ember.Mixin.create({

  handleApplicationError(error) {
    let details = this.handleUnprocessableEntities(error);
    details = details || this.handleBadRequest(error);
    this.controllerFor('application').setProperties({
      'errorMessage': error.message,
      'errorDetails': details || undefined
    });
  },

  handleBadRequest(error) {
    if (error.code !== 400 || !error.errors.length) { return; }
    // See https://github.com/cerebris/jsonapi-resources#error-codes
    let errors = error.errors.filterBy('code', 105);
    errors = errors.mapBy('detail');
    return (!errors) ? '' : errors.join(' ');
  },

  handleUnprocessableEntities(error) {
    if (error.code !== 422 || !error.errors.length) { return; }
    // See https://github.com/cerebris/jsonapi-resources#error-codes
    let errors = error.errors.filterBy('code', 100);
    let fields = errors.map(function(error) {
      let paths = error.source.pointer.split('/');
      let attr = paths[paths.length - 1].split('_');
      attr = attr.map(function(str) {
        return Ember.String.capitalize(str);
      });
      return attr.join(' ');
    });
    return (!fields) ? '' : 'Invalid fields: ' + fields.join(', ') + '.';
  }
});

For the admin/create route no error substate template was needed. For handling 500 errors the parent application error substate will be used. And for handling 400 or 422 responses it makes sense to display the errors in-context without making a transition to a substate. The only reason the admin.edit_error subsate uses the admin/edit/error.hbs template is to deal with a 404 and a 500 response. That is not the case with the admin.create route, the parent substate, application_error will work just fine.

Pay attention to the naming convention used with ember-cli, the substates use a . period and _ for the substate name and the templates use / and -. So to transition to the application error substate use application_error like so: this.intermediateTransitionTo('application_error', err) or to a nested substate: this.intermediateTransitionTo('admin.edit_error', err).

Discussion

The ApplicationRoute error action handler can be used to catch and handle various errors and delegate the displaying of the errors to the ApplicationController and accompanying HTMLBars application template.

Or, a route error substate with an *_error template can handle error events.

In the Ember JSONAPI Resources addon a FetchMixin defines error types for:

  • ServerError - handles 50x
  • ClientError - handles 40x
  • FetchError (default failure) - handles 30x

The error objects thrown by the server for a resource includes the error code. You can use the codes to determine how to present the error notification, use the error type, or use the error.name property. The error.code the most specific.

Based on the error code the route error action can transition to a custom route to handle that specific error type. In the first example a transition is made to a /not-found route, that could have used an intermediateTransitionTo to keep the URL unchanged.

However, ther is a catch when using a route's error action, by doing so the route error substates are not used. The second solution does not use use the route error action. Instead it uses route error substates and with error templates.

Using the error substates allows the use of the route-name_error state and associated route-name-error template. The setupController hook of the *_error route receives objects: controller and error (as it's model). Using the route error substate prototype to define properties for the error template is a good way to vary the display of the error notifications, based on error codes.

When the error (likely in a route) is not thrown by one of the model hooks, perhaps by a custom action, you can decide how to handle the error. If the error is recoverable perhpas set properties on the applicaiton controller for display by the applicaiton template. Or, if the error is not recoverable perhaps fire the error event on the route, e.g. this.send('error', resp);

(One caveat - the current state of using an acceptance test for an error substate is that the test may fail. Any error may cause your test adapter to fail the test. See 12791.)

Links

Clone this wiki locally