App wide error handling


#1

I’m curious how others deal with global error handling in your apps. I’m nearing the point of creating in-app error classes for various types of application errors (AppAjaxError, AppStringError, AppPermissionError, etc) and wondering if I’m overthinking my design, or if error classes are the next step of natural progression.

So let me backup and quickly cover what I have and my basic use cases.

What I have: An Ember.onerror definition which accepts application errors and rejected promises (obviously, it’s an ember hook :smiley:). In my onerror I do one or two things depending on the error and error type 1) Alert the user an error occurred via a service and 2) Log the error to the server.

My basic use cases: Some errors, such as exceptions and undefined state should be logged to the server and the user should be notified. Other errors, such as AJAX errors should only notify the user (server already has the log). Yet other errors, such realtime timeouts or background syncing, could be too noisy for users, but we still want to log to the server.

Normalizing: the error to present something valuable to the user. Most of the time with an AJAX error you have a nice message to give the user. But sometimes it still requires some normalization. Sometimes you have in-app validation errors, where you throw before ever hitting the server.

Promise Handling: In some cases a promise will fail with an error. A rejection handler chain to that promise might try to consume that error, say in the case of field validation it might add an error class to the associate input element. However if it can’t handle the error, it may just rethrow it to be handled later (ultimately by Ember.onerror).

So at this point I’ve shown the clear need to handle errors differently based on type and origin. Also hopefully I’ve shown that pure string errors won’t always cut it.

I’m sure many others with medium to large size apps have had this need. How did you solve it? What types of errors do you use with promise rejections? Any good general JS practices in this area?

Thanks!


#2

I know this topic is old but I am also surprised no one has taken up the challenge. I too have the same questions and concern. When I read this I realized there are some things / designs that can help here.

In one of our apps we had an Ember.onerror that posted to sentry.io. But we were getting so many false positives with TransactionAborted, ember-concurrency canceled errors, our own application errors, etc. We quickly realized that to find every invocation of these was too difficult so we needed a way to inspect and choose what to do at the top Ember.onerror function.

To do this we did two things. First any error we throw in our app needed to extend from a top level ApplicationError. We created an app/utils/errors folder. Our ApplicationError looked like this:

// For ember < 3.0
// app/utils/errors/application.js
import Ember from 'ember';

export default function ApplicationError(message) {
  Ember.Error.call(this, message);
  this.name = 'ApplicationError';
}
ApplicationError.prototype = Object.create(Ember.Error.prototype);

export function isApplicationError(error) {
  return error instanceof ApplicationError;
}
// For ember >= 3.0
// app/utils/errors/application.js
import EmberError from '@ember/error';

export default class ApplicationError extends EmberError {
  constructor(message) {
    super(message);
    this.name = 'ApplicationError';
  }
}
export function isApplicationError(error) {
  return error instanceof ApplicationError;
}

The second thing we did was to employ the strategy pattern in our Ember.onerror. We would wrap the error in a Strategy object which knew how to inspect different errors and return an appropriate handler. Some handlers were a no-op and some were reporting to our analytics.

If an expected error happened in our code we would handle the user notification there in the code. We left the Ember.onerror logic to uncaught, unexpected, or non user addressable errors.

// app/utils/error-handler.js
import { isApplicationError } from './errors/application';

const errorHandlers = new Set();

export default class ErrorHandler {
  constructor(error) {
    this.error = error;
  }
  shouldReport() {
    return true;
  }
  static create(error) {
    for (let Klass of errorHandlers) {
      if (Klass.predicate(error)) { return new Klass(error); }
    }
    return new DefaultHandler(error);
  }
  static register(Handler) {
    errorHandlers.add(Handler);
  }
}

export class DefaultHandler extends ErrorHandler {}

export class TransistionAbortHandler extends ErrorHandler {
  shouldReport() { return false; }
  static predicate(error) {
    return /TransitionAborted/.test(error.message);
  }
}

export class ApplicationErrorHandler extends ErrorHandler {
  shouldReport() {
    return this.error.severity === 'danger';
  }
  static predicate(error) {
    return isApplicationError(error);
  }
}

ErrorHandler.register(TransistionAbortHandler);
ErrorHandler.register(ApplicationErrorHandler);

And our Ember.onerror:

Ember.onerror = function onerror(error) {
  let handler = ErrorHandler.create(error);
  if (handler.shouldReport()) {
    Sentry.report(error);
  }
};

Or something like that.