framework: Add global error/exception handler

Aurelia should route any uncaught exception or any unhandled rejected promise coming from expressions or from methods called by the framework itself (attached, activate, constructor (DI)) to a global exception handling service in order to let application developers to log the errors.

This would be equivalent to Angular’s $exceptionHandler service.

This serves two purposes:

  1. At dev time, I can implement a handler that shows an alert whenever there is an uncaught exception. No need to constantly monitor the console window.
  2. At production time, I can implement a handler that forwards all uncaught exceptions to a logging service on my server. Moreover I could decide to show an error panel of some sort, something like “Oups! Something went wrong, please reload your browser.”

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Reactions: 3
  • Comments: 38 (14 by maintainers)

Most upvoted comments

With all due respect, I do not understand why this issue was closed. The need to present a clean UX to the user when an unhandled error occurs is a necessity in every single app as is the need to log the cause. @EisenbergEffect could please clarify?

Update to my previous post.

Another variation of case 2 is with a failed XHR Request, I was able to implement a workaround using https://github.com/slorber/ajax-interceptor which looks like this.

AjaxInterceptor.addResponseCallback((xhr)=> {
  if (xhr.status === 500) {
   ...
  }
});
AjaxInterceptor.wire();

I still have not found a workaround for Promises.

Until I get Aurelia’s team’s point of view on the issue, I keep the two workarounds inside my custom log appender.


The full code is this:

import {EventAggregator} from 'aurelia-event-aggregator';
import {inject} from 'aurelia-framework';
import AjaxInterceptor from 'ajax-interceptor'

@inject(EventAggregator)
export class UnhandledErrorPublisher {
  constructor(eventAggregator) {
    this.eventAggregator = eventAggregator;

    window.addEventListener('error', (errorEvent)=> {
      let msg = `${errorEvent.error.message} \r ${errorEvent.error.stack}`
      this.eventAggregator.publish('Unhandled-Error', msg);
    });

    AjaxInterceptor.addResponseCallback((xhr)=> {
      if (xhr.status === 500) {
        let msg = `${xhr.statusCode} - ${xhr.statusText} \r ${xhr.responseText}`
        this.eventAggregator.publish('Unhandled-Error', msg);
      }
      if (xhr.status === 0) {
        let msg = `XMLHttpRequest request cancelled by browser (status code 0). See console for details.`
        this.eventAggregator.publish('Unhandled-Error', msg);
      }
    });
    AjaxInterceptor.wire();
  }

  error(logger, message, ...rest) {
    this.eventAggregator.publish('Unhandled-Error', message, ...rest);
  }

  debug(logger, message, ...rest) {
  }

  info(logger, message, ...rest) {
  }

  warn(logger, message, ...rest) {
  }
}

which is wired-up this way:

import * as TheLogManager from 'aurelia-logging';
import {UnhandledErrorPublisher} from './unhandled-error-publisher.js'

export function configure(aurelia) {
   TheLogManager.addAppender(aurelia.container.get(UnhandledErrorPublisher));
  TheLogManager.setLevel(TheLogManager.logLevel.debug);

  aurelia.use
    .standardConfiguration()
    .developmentLogging();
  aurelia.start().then(a => a.setRoot());
}

I have another component that subscribes to the Unhandled-Error message and either shows the error in the view in a red panel or tries to send the error to a logging service on the backend (depending on whether or not my app is running in a debug or release configuration).

I figured out how to handle unhanded promise rejections:

    // handle core-js Promise rejection
    let baseOnunhandledrejection = window.onunhandledrejection;
    window.onunhandledrejection = (rejection)=>{
      let msg = `Unhandled promise rejection : ${rejection.reason}`;
      if (rejection.reason.stack) {
        msg += `\r${rejection.reason.stack}`;
      }

      this.eventAggregator.publish('unhandled-error', msg);

      if (baseOnunhandledrejection){
        baseOnunhandledrejection(data);
      }
    };

The full code is now:

import {EventAggregator} from 'aurelia-event-aggregator';
import {inject} from 'aurelia-framework';
import AjaxInterceptor from 'ajax-interceptor'

@inject(EventAggregator)
export class UnhandledErrorPublisher {
  constructor(eventAggregator) {
    this.eventAggregator = eventAggregator;

    window.addEventListener('error', (errorEvent)=> {
      let msg = `${errorEvent.error.message} \r ${errorEvent.error.stack}`
      this.eventAggregator.publish('Unhandled-Error', msg);
    });

    AjaxInterceptor.addResponseCallback((xhr)=> {
      if (xhr.status === 500) {
        let msg = `${xhr.statusCode} - ${xhr.statusText} \r ${xhr.responseText}`
        this.eventAggregator.publish('Unhandled-Error', msg);
      }
      if (xhr.status === 0) {
        let msg = `XMLHttpRequest request cancelled by browser (status code 0). See console for details.`
        this.eventAggregator.publish('Unhandled-Error', msg);
      }
    });
    AjaxInterceptor.wire();
  }

  // handle core-js Promise rejection
  let baseOnunhandledrejection = window.onunhandledrejection;
  window.onunhandledrejection = (rejection)=>{
    let msg = `Unhandled promise rejection : ${rejection.reason}`;
    if (rejection.reason.stack) {
      msg += `\r${rejection.reason.stack}`;
    }

    this.eventAggregator.publish('unhandled-error', msg);

    if (baseOnunhandledrejection){
      baseOnunhandledrejection(data);
    }
  };

  error(logger, message, ...rest) {
    this.eventAggregator.publish('Unhandled-Error', message, ...rest);
  }

  debug(logger, message, ...rest) {
  }

  info(logger, message, ...rest) {
  }

  warn(logger, message, ...rest) {
  }
}

According to the core-js doc, you also need this in index.html. Even without deleting the native window.Promise it worked with my test case but I guess it is safer to follow the recommendation.

    <script>
      // see https://github.com/zloirock/core-js
      // section : Unhandled rejection tracking
      // warning: If someone wanna use this hook everywhere - he should delete window.Promise before inclusion core-js.
      delete window.Promise;
    </script>
    <script src="jspm_packages/system.js"></script>
    <script src="config.js"></script>
    ...