-
-
Save ThomasBurleson/7514779 to your computer and use it in GitHub Desktop.
/** | |
* Implement a tryCatch() method that logs exceptions for method invocations AND | |
* promise rejection activity. | |
* | |
* @param notifyFn Function callback with logging/exception information (typically $log.error ) | |
* @param scope Object Receiver for the notifyFn invocation ( optional ) | |
* | |
* @return Function used to guard and invoke the targeted actionFn | |
*/ | |
function makeTryCatch( notifyFn, scope ) | |
{ | |
/** | |
* Report error (with stack trace if possible) to the logger function | |
*/ | |
var isObject = function (value) | |
{ | |
return value != null && typeof value == 'object'; | |
}, | |
reportError = function (reason) | |
{ | |
if(notifyFn != null) | |
{ | |
var error = (reason && reason.stack) ? reason : null, | |
message = reason != null ? String(reason) : ""; | |
if(error != null) | |
{ | |
message = error.message + "\n" + error.stack; | |
} | |
notifyFn.apply(scope, [message]); | |
} | |
return reason; | |
}, | |
/** | |
* Publish the tryCatch() guard 'n report function | |
*/ | |
tryCatch = function (actionFn, scope, args) | |
{ | |
try | |
{ | |
// Invoke the targeted `actionFn` | |
var result = actionFn.apply(scope, args || []), | |
promise = ( isObject(result) && result.then ) ? result : null; | |
// Catch and report any promise rejection reason... | |
if ( promise ) | |
{ | |
promise.then( null, reportError ); | |
} | |
actionFn = null; | |
return result; | |
} | |
catch(e) | |
{ | |
actionFn = null; | |
throw reportError(e); | |
} | |
}; | |
return tryCatch; | |
} |
Also here is the source for the Plunkr demo mentioned above:
angular.module('myApp', [ ])
.service( "videoPlayer", ["$interval", "$q", "$log", function($interval, $q, $log)
{
var onPlayRequested = function( url )
{
var dfd = new $q.defer(),
makeServerRespond = function()
{
$log.debug( "VideoPlayer::play( makeServerRespond() )" );
switch( url )
{
case "exception" :
throw new Error( "told to throw error" );
break;
case "invalid" :
dfd.reject( "told to reject");
break;
default :
dfd.resolve( url );
break;
}
},
onCheckResponse = function( response )
{
$log.debug( "VideoPlayer::play( onCheckResponse() )" );
// After the resolve(), conditionally throw an exception
if ( response == "notAuthorized" )
{
throw new Error( "rejected after resolve" );
}
return response;
};
$log.debug( "VideoPlayer::play( )" );
$interval( makeServerRespond, 60, 1 );
return dfd.promise
.then( onCheckResponse );
};
// Publish API
return {
play : onPlayRequested
};
}])
.controller( "TestController", ["$scope", "videoPlayer", "$log", function( $scope, videoPlayer, $log )
{
var announceReady = function( status )
{
$log.debug( "TestController::onResolve_play( " + status + " )" );
return status;
},
tryCatch = angular.makeTryCatch( $log.error );
$scope.play = function( url )
{
$log.debug( "TestController::play()" );
tryCatch( function()
{
return videoPlayer
.play( url )
.then( announceReady );
})
};
}]);
And finally,here is an AS3 version of the same: makeTryCatch.as
@IgorMinar posted this:
How is this different from the built in error logging? Are there scenarios where the built in stuff is insufficient?
It would be useful to have a side by side comparison of the your stuff with the equivalent functionality implemented using the built-in $q.
Here is the equivalent without the tryCatch()
macro:
try {
// Make async request to promise-returning `play()` API
var result = videoPlayer.play( url );
// Catch an exception or rejection and log it...
result.catch( function( reason )
{
var error = angular.isDefined(reason.stack ) ? reason : null,
message = angular.isString(reason) ? reason : reason.toString();
if ( error != null )
{
message = error.message + "\n" + error.stack;
}
$log.error( message );
// Continue rejection propagation... not needed in this scenario
// where the promise chain is orphaned.
return $q.reject( reason );
});
// Return the original promise chain
return result;
} catch( error ) {
// Log exceptions external to promises and rethrow
$log.error( error.message + "\n" + error.stack );
throw error;
}
AngularJS $q and $log have s-tons of functionality, but when developers want a macro function that works transparently with $q Promises and outputs rejections to the $log.error or a LogEnhancer, then tryCatch() becomes very useful.
Imagine a custom service API with 10 public methods (each return a promise). Without tryCatch()
developers would quickly lose their DRY code while attempting to log rejections and manage exceptions for each of the API calls. Developers could easily end up with lots of redundant code similar to that show above for the videoPlayer.play( url )
function.
And then we should consider the fact that logging exceptions in Promises is tricky. I contend that it is highly like that most developers are not aware of such subtle issues that require your to rethrow the exception if you do not want to interrupt the original rejection flow.
With the tryCatch()
macro, however, consider the resulting code savings:
var tryCatch = makeTryCatch( $log.error );
return {
play : function( url )
{
return tryCatch( function() {
return videoPlayer.play( url );
};
},
pause : function()
{
return tryCatch( function() {
return videoPlayer.pause();
};
},
seek : function( startAt:String )
{
return tryCatch( function() {
return videoPlayer.seek( startAt );
};
}
};
I have found this solution to be extremely useful and reduces developer code significantly. Btw - tryCatch()
call also be transparently used with APIs that do not return Promises; the same macro method for multiple purposes.
@igor, perhaps I am missing something already available in AngularJS or some other thought you were suggesting ?
See a AngularJS demo using
makeTryCatch()
@ http://plnkr.co/edit/21ytT4?p=info