Today I want to talk about a very interesting topic: how to handle errors that happens on backend. These may include database errors, validation errors, server faults, etc. There are two major situations when an error may occur, and these situations should be handled differently.
An error during a transition into a route (i.e. in a model hook)
When Ember.js performs a transition to a new route, it calls a model
method of that route. Here is a code of a typical route:
//..app/routes/posts.js import Ember from 'ember'; export default Ember.Route.extend({ model: function () { return this.store.findAll('post'); } });
In this example (given that post
model is defined, and backend is up and running), Ember will make a GET
request to /posts
and then render a posts
template with some data. But what if server will not respond or will respond with an error? A transition will fail and user either will stay on a previous page or will see a blank page (if they came directly to route with error). In both cases they will not understand what is going on and will become irritated. For these kind of situations it would be great to create a nice error page with an error message and a “Try again” button on it.
The first part of a task (create an error page) is easy, because if an error happens during transition, Ember will try to render an error
template (documentation). We just need to create such a template:
{{!--//..app/templates/error.hbs--}} <h2>{{model.errors.[0].status}}: {{model.errors.[0].detail}}</h2>
Here is a screenshot of an error page (of course, it’s appearance can be changed with css):
As for a “Try again” button, more coding is necessary. We need to abort a failed transition, store it and retry it after user presses a button.
First, let’s create a service to save a failed transition:
//..app/services/memory-storage.js import Ember from 'ember'; export default Ember.Service.extend({ getAndRemove: function (key) { var obj = this.get(key); if (obj !== undefined) { this.set(key, undefined); } return obj; } });
This service is useful to store any object in memory and get access to it from anywhere. It inherits methods from Ember.Object
, so .set
, .get
and other methods are available. I also added a new method, .getAndRemove
, that allows to get an object from a storage and then “remove” it, replacing with undefined
.
From Ember’s documentation you may already know that before rendering an error
template, Ember will fire an error
event and pass an error object and a failed transition to it’s handler. The best place to handle this event is an application
route, because it is a parent of all routes in any Ember app:
//..app/routes/application.js import Ember from 'ember'; export default Ember.Route.extend({ memoryStorage: Ember.inject.service('memory-storage'), //inject a storage service, created ealier actions: { error: function (error, transition) { console.log('Application route error handler', error, transition); transition.abort(); //abort transition, so it can be retried later this.get('memoryStorage').set('failedTransition', transition); //save transition to storage return true; //return true to display an error page that we created } } });
UPD: In newer Ember versions (I found this in 2.4.2) “transition.abort();” line causes an error and should be removed.
UPD2: In 2.8 you don’t need to remove “transition.abort();”, it works fine with it.
Now, let’s create an error
controller with a retry
action and a couple of useful computed properties:
//..app/controllers/error.js import Ember from 'ember'; export default Ember.Controller.extend({ memoryStorage: Ember.inject.service('memory-storage'), //inject a storage service /** * These computed properties will help to handle a special case, * when there is no error status and message (usually when internet * connection is gone) */ status: Ember.computed('model', function () { if ( this.model !== undefined && this.model.errors !== undefined && this.model.errors.length > 0 && this.model.errors[0].status !== undefined ) { return this.model.errors[0].status; } else { return 0; } }), message: Ember.computed('model', function () { if ( this.model !== undefined && this.model.errors !== undefined && this.model.errors.length > 0 ) { for (var i = 0; i < this.model.errors.length; i++) { if (this.model.errors[i].detail !== undefined && this.model.errors[i].detail !== '') { return this.model.errors[i].detail; } } } return undefined; }), actions: { retry: function () { //get a failed transition from storage var transition = this.get('memoryStorage').getAndRemove('failedTransition'); if (transition !== undefined) { transition.retry(); //retry transition } } } });
And modify an error
template:
{{!--..app/templates/error.hbs--}} <h2> {{#if message}} {{status}}: <small>{{message}}</small> {{else}} Your internet connection is gone {{/if}} </h2> <div> <button {{action 'retry'}}>Try again</button> </div>
A final version of an error page (with internet switched off) looks like:
An error after a transition to a route
If an error happens after a transition is made (i.e. we call a model.save()
in one of a controller’s actions and backend does not respond), the previous solution will not work, as Ember will not fire an error
event. In this case we need to use a .then
method from a RSVP.Promise
class. This method accepts two arguments: a success handler and a failure handler. Take a look at an example:
//..app/controllers/index.js import Ember from 'ember'; export default Ember.Controller.extend({ actions: { generatePost: function () { var post = this.get('store') .createRecord('post'); post.set('title', 'Generated post'); post.set('text', 'I like Ember.js'); post.save().then(undefined, (error) => { console.log(error); }); } } });
In order to inform a user about an error, we need to extract errors from an error
object and display a message somehow (I will use an alert to simplify things, but there is some addons for nice flash messages on GitHub). Given the fact, that there can be a lot of such places in an application, we need to create a method, that will accept an error
object and will be available from any controller or route. It’s easy to do with a mixin:
//..app/mixins/handle-errors.js import Ember from 'ember'; export default Ember.Mixin.create({ handleErrors: function (error) { console.log(error); //dump an error object to see it's structure for (var i = 0; i < error.errors.length; i++) { if (error.errors[i].detail !== undefined && error.errors[i].detail !== '') { //use some addon to show a nice message, I'll use an alert alert(error.errors[i].detail); } } } });
Using a mixin is simple:
//..app/controllers/index.js import Ember from 'ember'; import ErrorHandler from '../mixins/error-handler';//Import a mixin //Pass a mixin as the first parameter to extend method export default Ember.Controller.extend(ErrorHandler, { actions: { generatePost: function () { var post = this.get('store') .createRecord('post'); post.set('title', 'Generated post'); post.set('text', 'I like Ember.js'); post.save().then(undefined, (error) => { this.handleErrors(error); post.rollbackAttributes(); //As saving is failed, we need to rollback changes }); } } });
A few words about an error object
A structure of an error
object may be different, depending on Ember version and a backend response. If you use Ember 1.13.x or 2.x and your backend returns errors as a string with 400x status, an error
object will look like
{ "message": "Adapter operation failed", "name": "Error", "errors": [ { "status": "404", "title": "The backend responded with an error", "detail": "Cannot POST /posts" } ] }
But if your server responds with some JSON, it is possible that you will get something different in your error handler. It’s a good idea to use console to see what do you get and adopt your code to situation.
I created an application, that demonstrates these techniques, so you may run it and see how it works for yourself (GitHub repository).
That’s all. Have a nice code!
Featured image source – pxhere.com