Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement a proper global HTTP error handling in ember

Tags:

ember.js

What I want: an error handling that handles different http errors (401, 404, 500) globally. It shouldn't matter where or when an http error occurs.

So far I implemented an error action in the application route that will be called on any adapter errors on route's model hook. That's working fine.

What is not covered is the case when I work with records in other contexts like record.save(). There I need to separately handle the error on the promise.

Moreover I don't only want to have a default error handler but more like a fallback.

Ok, before talking too much let's have an example implementation of my use cases.

application route

The application error action should be the default / fallback error handler.

actions: {
  error: function(error) {
    var couldHandleError = false;

    if (error.errors) {
      switch (error.errors[0].status) {
        case '401':
          // User couldn't get authenticated.
          // Redirect handling to login.

          couldHandleError = true;
          break;
        case '404':
        case '500':
          // Something went unexpectedly wrong.
          // Let's show the user a message

          couldHandleError = true;
          break;
      }
    }

    // return true if none of the status code was matching
    return !couldHandleError;
  }
}

Some route

In this case the application error action is called.

model: function() {
  return this.store.findAll('post');
}

Some controller

In this case the application error action is NOT called.

(I know, the following code probably doesn't make sense, but it is just supposed to illustrate my requirements)

this.store.findRecord('post', 123);

Some other controller

In this example the application error action is not called, for sure, since I use my own handler here (catch()). But as you can see in the comments I do want to use the default handler for all status codes other than 404.

this.store.findRecord('post', 123).catch(function(reason) {
  if (reason.errors[0].status === '404') {
    // Do some specific error handling for 404
  } else {
    // At this point I want to call the default error handler
  }
});

So is there a clean and approved way of achieving that? I hope I could make my problem clear to you.

like image 602
val Avatar asked Aug 18 '15 17:08

val


2 Answers

You can try to do something with these events (put these lines in app.js before app initialization):

Ember.onerror = function (error) {
  console.log('Ember.onerror handler', error.message);
};

Ember.RSVP.on('error', function (error) {
  console.log('Ember.RSVP error handler', error);
});

Ember.Logger.error = function (message, cause, stack) {
  console.log('Ember.Logger.error handler', message, cause, stack);
};

I learned about them from https://raygun.io/blog/2015/01/javascript-error-handling-ember-js/, you may find some details there.

like image 172
Gennady Dogaev Avatar answered Nov 09 '22 19:11

Gennady Dogaev


I think I have my final solution I want to share with you. Basically I took the ideas of the guys commenting my question and extended them so they fit my needs.

First I created a mixin with the main logic. Since I want it to be as generic as possible, it distinguishes between a) controller / route and b) jquery / adapter error. So it doesn't matter from where you call it and whether your error object is originally from an jquery Ajax request or an ember adapter.

import Ember from 'ember';

export default Ember.Mixin.create({

  ajaxError: function(error) {
    if (!error) {
      Ember.Logger.warn('No (valid) error object provided! ajaxError function must be called with the error object as its argument.');
      return;
    }

    // Depending whether the mixin is used in controller or route
    // we need to use different methods.
    var transitionFunc = this.transitionToRoute || this.transitionTo,
        couldHandleError = false;

    switch (this._getStatusCode(error)) {
      case 401:
        transitionFunc.call(this, 'auth.logout');
        couldHandleError = true;
        break;
      case 404:
      case 500:
        // Here we trigger a service to show an server error message.
        // This is just an example and currently not the final implementation.
        // this.get('notificationService').show(); 
        couldHandleError = true;
        break;
    }

    // For all other errors just log them.
    if (!couldHandleError) {
      Ember.Logger.error(error);
    }
  },

  _getStatusCode: function(error) {
    // First check for jQuery error object
    var status = error.status;

    // Check for ember adapter error object if it's not a jquery error
    if (!status && error.errors && error.errors[0].status) {
      status = parseInt(error.errors[0].status);
    }

    return status;
  },

});

Next I reopened some Classes (inside app.js) to make this functionality globally available:

import AjaxErrorMixin from 'app/mixins/ajax-error';

Ember.Route.reopen(AjaxErrorMixin);
Ember.Controller.reopen(AjaxErrorMixin);

Ember.Component.reopen({
  _actions: {
    // Passing ajaxError per default
    ajaxError: function(error) {
      this.sendAction('ajaxError', error);
    }
  }
});

Finally I added some actions to the application route:

actions: {
  error: function(error) {
    this.send('ajaxError', error);
  },

  ajaxError: function(error) {
    this.ajaxError(error);
  },
}

Why do I have two actions doing the same stuff? Well, the error action is called on errors on route's model hook. I could stay with that action, but in the rest of the application where I explicitly call this action I want a more meaningful name. Therefore I also created a ajaxError action. You could stay with one action, for sure.

Now you can use this everywhere:

Route / Controller:

this.ajaxError(error);

Component:

this.sendAction('ajaxError', error);

For sure, you also need to pass the action out of the component to be handled by the application route:

{{some-component ajaxError="ajaxError"}}

This works for nested components, too. You don't need to explicitly send this action further inside the component.js file since we reopened the Component and passt this action into.

I hope I can help other people with that implementation. Also any feedback is welcome.

like image 40
val Avatar answered Nov 09 '22 17:11

val