Skip to content

Instantly share code, notes, and snippets.

@ThomasBurleson
Last active December 28, 2015 14:29
  • Star 9 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save ThomasBurleson/7514779 to your computer and use it in GitHub Desktop.
Using Promises and AngularJS $log, I demonstrate how to guard targeted function invocations using a `super` version of try-catch. And the guard will report exceptions (with stack traces) via the logger function. If the target function returns a promise, then a rejection handler is attached; which will also report rejections and possible stack tr…
/**
* 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;
}
@ThomasBurleson
Copy link
Author

Now with AngularJS $log and a promise-returning API, here is an amazingly simple use to log exceptions or promise rejections.

    var tryCatch = makeTryCatch( $log.error );

    tryCatch( function() {

        /**
        * Note:  Since `play()`  returns a promise, then our wrapper function must 
        * return the Promise so `tryCatch` can attach and `report` any future rejections
        */
        return videoPlayer.play( "http://youtube.com/channels/ComedyCentral" );

    }, this );

With this construct, developers have 4 conditions accounted for:

  1. play() resolves with no issues...
  2. play() rejects with some issues
  3. play() resolves() but then is rejected (via thrown exception)
  4. play() throws an exception outside of the promise handlers

Gorgi Korsev said: "Promises are throw-safe. If an error is thrown in one of the .then callbacks, only that single promise chain will die."

A sample console output could be:

Start - TestController::play() angular.js:9101
   Start - VideoPlayer::play( invalid ); angular.js:9101
   Done - VideoPlayer::play() angular.js:9101
Error: invalid URL
    at onResolved (http://run.plnkr.co/mFPNkUCrVwD4Khhb/bootstrap.js:15:53)
    at Array.wrappedProgressback (http://code.angularjs.org/1.2.1/angular.js:10615:88)
    at http://code.angularjs.org/1.2.1/angular.js:10583:28
    at Scope.$eval (http://code.angularjs.org/1.2.1/angular.js:11576:28)
    at Scope.$digest (http://code.angularjs.org/1.2.1/angular.js:11421:31)
    at Scope.$apply (http://code.angularjs.org/1.2.1/angular.js:11682:24)
    at tick (http://code.angularjs.org/1.2.1/angular.js:8189:36) angular.js:9101

@ThomasBurleson
Copy link
Author

See a AngularJS demo using makeTryCatch() @ http://plnkr.co/edit/21ytT4?p=info

@ThomasBurleson
Copy link
Author

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 );
                })
            };

       }]);

@ThomasBurleson
Copy link
Author

And finally,here is an AS3 version of the same: makeTryCatch.as

@ThomasBurleson
Copy link
Author

@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 ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment