<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular-animate.js"></script>
    <script src="//angular-ui.github.io/bootstrap/ui-bootstrap-tpls-2.5.0.js"></script>
    <link href="//netdna.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
    <script src="scripts/errorDemoApp.module.js"></script>
    <script src="scripts/services/demo.service.js"></script>
    <script src="scripts/services/exception-handler.service.js"></script>
    <script src="scripts/services/custom-exception-handler.service.js"></script>
    <script src="scripts/components/retriever/retriever.component.js"></script>
    <script src="scripts/components/popup/popup.component.js"></script>
  </head>
  <body>
    <div ng-app="errorDemoApp">
      <retriever></retriever>
    </div>
  </body>
</html>
<div>
    <label>Get Data</label>
    <br/>
    <button ng-click="ctrl.getUsers()">Get users</button>
    <br>
    <button ng-click="ctrl.getToDos()">Get todos</button>
    <br />
    <button ng-click="ctrl.getComments()">Get comments</button>
    <br />
    <hr />
    <br />
    <pre>{{ctrl.result}}</pre>
</div>
errorDemoApp.component('retriever', {
  templateUrl: 'scripts/components/retriever/retriever.component.html',
  controller: ('retrieverController', retrieverController),
  controllerAs: 'ctrl',
});

function retrieverController(demoService, customExceptionHandler) {
  let self = this;

  self.result = '';
  self.getUsers = getUsers;
  self.getToDos = getToDos;
  self.getComments = getComments;

  //exceptions will be caught in the component for two scenarios
  //1. when we want to show a specific error message to a user
  //2. when we want to log some specific/targetted data about the exception

  function getUsers() {
    //scenario 1
    //we need to show a specific error message to the user in this function
    demoService.getUsers()
      .then((response) => {
        self.result = response;
      })
      .catch((error) => {        
        customExceptionHandler.showErrorMessage("Failed to retreive users");
        //now this error will be handled by $exceptionHandler override
        throw error;
      });
  }

  function getToDos() {
    //scenario 2
    //we will handle the error in this method it self and add some targeted information
    demoService.getToDos()
      .then((response) => {
        self.result = response;
      })
      .catch((error) => {
        //we log the error by calling the api that persists data (e.g. kibana)
        console.error(`an error happned in dataRetrieverController.getUsers: ${JSON.stringify(error)}`);
      });
  }

  function getComments() {
    //we dont need to always handle exceptions in file
    //if an error gets thrown here it will be handled by $exceptionHandler override
    demoService.getComments()
      .then((response) => {
        self.result = response;
      });
  }
}
<div class="modal-header">
    <h3 class="modal-title">An error occured</h3>
</div>

<div class="modal-body">
    {{ctrl.errorMessage}}
</div>

<div class="modal-footer">
    <button class="btn btn-primary" type="button" ng-click="ctrl.close()">Ok</button>
</div>
errorDemoApp.component('popup',{
  templateUrl:'scripts/components/popup/popup.component.html',
  controller:('popupController', popupController),
  controllerAs:'ctrl',
  bindings:{
    modalInstance: '<',
    resolve:'<',
  }
});

function popupController(){
  let self=this;

  self.errorMessage='';
  self.$onInit=$onInit;
  self.close=close;

  function $onInit(){
    self.errorMessage=self.resolve.message;
  }

  function close(){
    self.modalInstance.close(self.editedName);
  }
}
errorDemoApp.service('demoService',demoService);
//by not handling exceptions here, we can avoid cluttering the http layer
function demoService($http){
    let service={
        getUsers:getUsers,
        getToDos: getToDos,
        getComments: getComments
    }
    return service;

    function getUsers(){
         return $http.get("https://jsonplaceholder.typicode.com/users")
            .then(function (response) {
                // return response.data;
                return null.data;
            });
    }

    function getToDos() {
        return $http.get("https://jsonplaceholder.typicode.com/todos")
            .then(function (response) {
                // return response.data;
                return null.data;
            });
    }

    function getComments() {
        return $http.get("https://jsonplaceholder.typicode.com/comments")
            .then(function (response) {
                // return response.data;
                return null.data;
            });
    }

}
errorDemoApp.service('customExceptionHandler', customExceptionHandler);

function customExceptionHandler($uibModal) {
  let service = {
    showErrorMessage: showErrorMessage,
  };
  return service;

  function showErrorMessage(errorMessage){
    let popupSettings = {
        backdrop : 'static',
        keyboard : true,
        component: 'popup',
        resolve :{
          message: function(){
            return errorMessage;
          }
        }
      }

      let modalInstance=$uibModal.open(popupSettings);
      return modalInstance;
  }
}
//we are overriding the default angularJS $exceptionHandler here
errorDemoApp.factory('$exceptionHandler', exceptionHandler);

function exceptionHandler() {
  return function (exception, cause) {
    //we log the error by calling the api that persists data (e.g. kibana)
    console.error('following error occured:');
    console.error(`exception: ${exception}`);
    console.error(`cause: ${cause}`);
  };
}

//according to exceptionHandler documentaiton
//code executed in event-listeners (e.g. event handlers and async functions (like setTimeout(), setInterval(), callbacks) does not delegate exceptions to $exceptionHandler
//To really catch these errors it is necessary to add a global error handler in the browser, by setting the onerror property of the window object
//https://www.bennadel.com/blog/2855-piping-global-errors-into-the-exceptionhandler-service-in-angularjs.htm

errorDemoApp.run(function addGlobalErrorHandler($window, $exceptionHandler) {
  $window.onerror = function handleGlobalError(message, fileName, lineNumber, columnNumber, error) {
    // If this browser does not pass-in the original error object, let's
    // create a new error object based on what we know.
    if (!error) {
      error = new Error(message);

      // NOTE: These values are not standard, according to MDN.
      error.fileName = fileName;
      error.lineNumber = lineNumber;
      error.columnNumber = columnNumber || 0;
    }

    // Pass the error off to our core error handler.
    $exceptionHandler(error);
  };
});
var errorDemoApp = angular.module("errorDemoApp", ['ngAnimate', 'ui.bootstrap']);