I am currently exploring possible methods to handle application-wide exceptions in AngularJS.
One of the things we really wanted to avoid was wrapping multiple parts of the application in nested try/catch blocks, but handle things cleanly - i.e throw an exception in response to a promise.
Progress so far
A few short design goals:
The current leaning of my team is to write a service to handle exceptions, which would expose a range of simple calls:
exceptionService.warn('exception_token'); exceptionService.crit('another_exception_token');
This service would then format an 'exception' object and broadcast this from the rootscope. This would allow a default handler to watch for any broadcasts and apply default actions, as well as allow custom listeners to be set in others scopes, which could handle more specific conditions - i.e. disable a part of the UI.
var exception = { token: 'exception_token', severity': 'crit' }; // broadcast exception $rootScope.$broadcast( 'application_exception', exception );
One traditional way of handling errors in Angular is to provide an ErrorHandler class. This class can be extended to create your own global error handler. This is also a useful way to handle all errors that occur, but is mostly useful for tracking error logs.
AngularJS also includes built-in $exceptionHandler service, which handles uncaught exceptions in the application. The default implementation of $exceptionHandler service logs the exception into the browser console. You can override this service as per your requirement.
I was thinking about the same recently, and it occurred to me that when it comes to a good error handling in javascript, it is irrelevant which framework you are using, Angular on something else. I wrote one such error handler recently for an AngularJS project, but I did it in a way it can be used in any framework.
Here's the complete code. You can either use it directly, or modify to your needs...
/* Factory errorFact is to simplify error handling and reporting in other objects. It supports detailed error output as a text string and into the browser's console. Usage example: A function that supports return of an error object would have the following declaration as its very first line: var e = errorFact.create("objectName.funcName", arguments); - in this declaration we specify the full object + method name as the first string parameter, - and as the second parameter we pass javascript's reserved variable called arguments, which provides reference to all of the function's parameters for logging. When an error occurs, the function would return: return e.error("Error description text"); - this line will create and return a complete error context. When a function that supports return of an error object makes a call into another function that also supports the error context, then it can return the nested error result by passing the embedded error to the current error object instead of the error text. Example: var e = errorFact.create("objectName.funcName", arguments); var data = callAnotherFunc(...); // calling a function that support an error object; if(data.isError){ // If an error was triggered; return e.error(data); // return that error from the current context; } The top-level code that calls an error-returning function would do verification and if an error occurred, log all its details into console (typically). Example: var data = getData(...); if(data.isError){ data.log(); // Output all the error details into the browser's console; } */ "use strict"; app.factory("errorFact", function(){ return { // creates a new error context; create: function(method, args){ var result = { // initiates and returns the error context; error: function(msg){ this.info.isError = true; if(msg.isError){ this.info.details.caller = msg; }else{ this.info.details.msg = msg; } return this.info; }, info: { isError: false, details: {}, log: function(){ if(this.isError){ console.error(this.format()); } }, // formats complete error details into a text string; format: function(){ if(this.details.caller){ var txt = this.details.caller.format(); txt += "\nCALLER: " + this.details.method + "(" + this.formatArguments() + ")"; return txt; } if(this.details.method){ return "Error calling " + this.details.method + "(" + this.formatArguments() + "): " + this.details.msg; }else{ return this.details.msg; } return ""; }, // formats function argument details into a text string; formatArguments: function(){ if(!this.details.args){ return ""; } var params = ""; for(var i = 0;i < this.details.args.length;i ++){ if(params.length > 0){ params += ","; } var p = this.details.args[i]; if(p === undefined){ params += "undefined"; }else{ if(p === null){ params += "null"; }else{ if(typeof(p) == "object"){ params += "Object"; }else{ params += p; } } } } return params; } } }; if(method){ result.info.details.method = method; } if(args){ result.info.details.args = args; } return result; } } });
Below is a factory that shows how it is used:
"use strict"; app.factory('moduleFact', ['errorFact', function(errorFact){ return { // Locates existing module and expands its key Id references // into corresponding object references: // - If 'hintGroupId' is present, property 'hints' is added from // the corresponding hint group. // - If 'repModules' is present, properties 'question' and 'refs' // are added. // On success, return the expanded module object. // On failure, returns an error object. // // NOTE: Currently supports only the first value in repModules. expandModule: function(moduleData, moduleId){ var e = errorFact.create("moduleFact.expandModule", arguments); if(!moduleData || !moduleData.modules || !moduleId){ return e.error("Invalid parameters passed"); } var mod = this.findModule(moduleData, moduleId); if(mod.isError){ return e.error(mod); } var src = mod; if(mod.repModules){ var repId = mod.repModules[0]; if(!repId){ return e.error("Invalid repModules encountered"); } /////////////////////////////////////// // temporary check to throw a warning: if(mod.repModules.length > 1){ console.warn("Multiple values in property repModules: " + JSON.stringify(mod.repModules) + ", which is not supported yet (only the first value is used)"); } /////////////////////////////////////// src = this.findModule(moduleData, repId); if(src.isError){ return e.error(src); } } if(src.question){ mod.question = src.question; }else{ return e.error("Question not specified"); } if(src.refs){ mod.refs = src.refs; } if(src.hintGroupId){ var hg = this.findHintGroup(moduleData, src.hintGroupId); if(hg.isError){ return e.error(hg); } mod.hints = hg.hints; } return mod; // needed extra: expand attribute repModules }, // Expands all the modules and returns the data; expandAllModules: function(moduleData){ var e = errorFact.create("moduleFact.expandAllModules", arguments); if(!moduleData || !moduleData.modules){ return e.error("Invalid parameters passed"); } for(var i = 0;i < moduleData.modules.length;i ++){ var result = this.expandModule(moduleData, moduleData.modules[i].id); if(result.isError){ return e.error(result); } } return moduleData; }, // Locates and returns module by its Id; findModule: function(moduleData, moduleId){ var e = errorFact.create("moduleFact.findModule", arguments); if(!moduleData || !moduleData.modules || !moduleId){ return e.error("Invalid parameters passed"); } for(var i = 0;i < moduleData.modules.length;i ++){ if(moduleData.modules[i].id == moduleId){ return moduleData.modules[i]; } } return e.error("Module with Id = " + moduleId + " not found"); }, // Locates and returns Hint Group by its Id; findHintGroup: function(moduleData, hintGroupId){ var e = errorFact.create("moduleFact.findHintGroup", arguments); if(!moduleData || !moduleData.hintGroups || !hintGroupId){ return e.error("Invalid parameters passed"); } for(var i = 0;i < moduleData.hintGroups.length;i ++){ if(moduleData.hintGroups[i].id == hintGroupId){ return moduleData.hintGroups[i]; } } return e.error("Hint Group with Id = " + hintGroupId + " not found"); } } }]);
So, when you have such factory in place, your high-level code, such as in a controller would just log any issues as shown in the example below:
"use strict"; app.controller('standardsCtrl', ['$scope', 'moduleFact', function($scope, moduleFact){ var data = ...//getting data; var mod = moduleFact.expandAllModules(data); if(mod.isError){ mod.log(); // log all error details into the console; }else{ // use the data } }); }]);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With