Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFinNC 2009 (Raleigh, North Carolina) with: Larry Lyons
Ben Nadel at CFinNC 2009 (Raleigh, North Carolina) with: Larry Lyons

Monkey-Patching The $q Service With .fcall() In AngularJS

By Ben Nadel on

Yesterday, I looked at the pitfalls of starting an AngularJS promise-chain if the promise-generating method might throw an error. In that post, I solved the problem by wrapping the initial method inside a .then() callback; but, what I'd really like is a method akin to .fcall() in the Q-promise library. So, I wanted to see if I could monkey-patch the $q service, at runtime, to include a .fcall()-inspired method for function invocation.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

The concept behind .fcall() - at least in my demo - is that I want to start a promise chain by invoking a method that returns a promise; however, there's a chance that the initial method invocation will throw an error. In order to prevent that error from bubbling up, uncaught, I want to be able to catch it and translate it into a rejected promise. To do this, we defer to .fcall() to carry out the invocation in a protected context and ensure that a promise - either resolved or rejected - is returned.

My .fcall() method can take a variety of signatures:

  • .fcall( methodReference )
  • .fcall( methodReference, argsArray )
  • .fcall( context, methodReference, argsArray )
  • .fcall( context, methodName, argsArrray )
  • .fcall( context, methodReference )
  • .fcall( context, methodName )

The .fcall() method is going to be monkey-patched onto the $q service. In order to do that, we need to modify $q in a .run() block right after the AngularJS application is bootstrapped. This way, the modification will be available for any other component, within the application, that gets the $q service dependency-injected.

To see this in action, I'm starting a promise chain by calling loadSomething() with a set of arguments that will precipitate an error. This error will result in a promise that is rejected which will, in turn, cause my rejection handler to be invoked.

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Monkey-Patching The $q Service With .fcall() In AngularJS
  • </title>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Monkey-Patching The $q Service With .fcall() In AngularJS
  • </h1>
  •  
  • <p>
  • <em><storng>Note</strong>: This is not exactly the .fcall() method from Q.
  • Rather, this is inspired by that concept.</em>
  • </p>
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.3.8.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I monkey-patch the .fcall() method into the root of the $q service. We have
  • // to do this in a .run() block so that it will modify the $q service before any
  • // other component in the application needs it.
  • app.run(
  • function monkeyPatchQService( $q, $exceptionHandler ) {
  •  
  • // I invoke the given function using the given arguments. If the
  • // invocation is successful, it will result in a resolved promise; if it
  • // throws an error, it will result in a rejected promise, passing the
  • // error object through as the "reason."
  • // --
  • // The possible method signatures:
  • // --
  • // .fcall( methodReference )
  • // .fcall( methodReference, argsArray )
  • // .fcall( context, methodReference, argsArray )
  • // .fcall( context, methodName, argsArrray )
  • // .fcall( context, methodReference )
  • // .fcall( context, methodName )
  • $q.fcall = function() {
  •  
  • try {
  •  
  • var components = parseArguments( arguments );
  • var context = components.context;
  • var method = components.method;
  • var inputs = components.inputs;
  •  
  • return( $q.when( method.apply( context, inputs ) ) );
  •  
  • } catch ( error ) {
  •  
  • // We want to pass the error off to the core exception handler.
  • // But, we want to protect ourselves against any errors there.
  • // While it is unlikely that this will error, if the app has
  • // added an exception interceptor, it's possible something could
  • // go wrong.
  • try {
  •  
  • $exceptionHandler( error );
  •  
  • } catch ( loggingError ) {
  •  
  • // Nothing we can do here.
  •  
  • }
  •  
  • return( $q.reject( error ) );
  •  
  • }
  •  
  • };
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I parse the .fcall() arguments into a normalized structure that is
  • // ready for consumption.
  • function parseArguments( args ) {
  •  
  • // First, let's deal with the non-ambiguous arguments. If there are
  • // three arguments, we know exactly which each should be.
  • if ( args.length === 3 ) {
  •  
  • var context = args[ 0 ];
  • var method = args[ 1 ];
  • var inputs = args[ 2 ];
  •  
  • // Normalize the method reference.
  • if ( angular.isString( method ) ) {
  •  
  • method = context[ method ];
  •  
  • }
  •  
  • return({
  • context: context,
  • method: method,
  • inputs: inputs
  • });
  •  
  • }
  •  
  • // If we have only one argument to work with, then it can only be a
  • // direct method reference.
  • if ( args.length === 1 ) {
  •  
  • return({
  • context: null,
  • method: args[ 0 ],
  • inputs: []
  • });
  •  
  • }
  •  
  • // Now, we have to look at the ambiguous arguments. If w have
  • // two arguments, we don't immediately know which of the following
  • // it is:
  • // --
  • // .fcall( methodReference, argsArray )
  • // .fcall( context, methodReference )
  • // .fcall( context, methodName )
  • // --
  • // Since the args array is always passed as an Array, it means that
  • // we can determine the signature by inspecting the last argument.
  • // If it's a function, then we don't have any argument inputs.
  • if ( angular.isFunction( args[ 1 ] ) ) {
  •  
  • return({
  • context: args[ 0 ],
  • method: args[ 1 ],
  • inputs: []
  • });
  •  
  • // And, if it's a string, then don't have any argument inputs.
  • } else if ( angular.isString( args[ 1 ] ) ) {
  •  
  • // Normalize the method reference.
  • return({
  • context: args[ 0 ],
  • method: args[ 0 ][ args[ 1 ] ],
  • inputs: []
  • });
  •  
  • // Otherwise, the last argument is the arguments input and we know,
  • // in that case, that we don't have a context object to deal with.
  • } else {
  •  
  • return({
  • context: null,
  • method: args[ 0 ],
  • inputs: args[ 1 ]
  • });
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • app.controller(
  • "AppController",
  • function( $q ) {
  •  
  • // Invoke the loadSomething() method with given arguments - .fcall() will
  • // return a promise even if the method invocation fails.
  • $q.fcall( loadSomething, [ 1, 2, 3 ] )
  • .then(
  • function handleResolve( value ) {
  •  
  • console.log( "Resolved!" );
  • console.log( value );
  •  
  • },
  • function handleReject( error ) {
  •  
  • console.log( "Rejected!" );
  • console.log( error );
  •  
  • }
  • )
  • ;
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I load some data and return a promise.
  • function loadSomething( a, b, c ) {
  •  
  • // Using this special case to demonstrate the FAILURE path that
  • // will raise an exception (to see if .fcall() can catch it).
  • if ( ( a === 1 ) && ( b === 2 ) && ( c === 3 ) ) {
  •  
  • throw( new Error( "InvalidArguments" ) );
  •  
  • }
  •  
  • return( $q.when( "someValue" ) );
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

When I invoke the loadSomething() method with arguments [1,2,3], it will throw an error. However, .fcall() will catch it, turn it into a rejected promise, and cause our rejection handler to be invoked. As such, when we run the above code, we get the following output:


 
 
 

 
 The $q service, monkey-patched with .fcall(), can catch errors and turn them into rejected promises. 
 
 
 

The first line is the error being handed off to the core $exceptionHandler() service. The second line, however, is our rejection handler receiving the error-cum-rejected-promise.

While a method like .fcall() requires a different form of method invocation, I find it to be quite readable. It gets the job done and without all the cruft that my .then() approach had yesterday. Now, I can safely invoke promise-generating methods without the fear of uncaught exceptions.




Reader Comments

Ben,
It would be nice to have this patch as a stand alone bower / npm module, because I want to use it.

Reply to this Comment

For this kind of monkey-patching, you can use a decorator: https://docs.angularjs.org/api/auto/service/$provide#decorator

Something like this is a reusable module you can drop into your projects:

appModule.config(['$provide', function($provide) {
$provide.decorator('$q', ['$delegate', function($delegate) {
$delegate.fcall = //...
return $delegate;
})];
}]);

This configures the option early on, and guarantees the functionality is available to all your other Angular services and components.

Reply to this Comment

@Phil,

This is a great point. Tomasz Stryjewski was just recommending this on Twitter as well. I don't think I've ever used a decorator before. Actually, I believe I did a long time ago with HTTP request / response interceptors... but, if I recall correctly, that was basically copy/pasting from something I read.

The decorator looks like just the ticket. I'll definitely follow up with that exploration. Thanks!

Reply to this Comment

@Gleb,

That's a really interesting idea, but it doesn't seem to play nicely with Firefox (probably because I have Firebug installed?). But, it seems to work in Chrome. Very cool!

Reply to this Comment

@Tomasz, @Phil,

Thanks again for the feedback - decorators look pretty cool!

http://www.bennadel.com/blog/2775-monkey-patching-the-q-service-using-provide-decorator-in-angularjs.htm

To be honest, I've never really had a great mental model for what the configuration phase is and/or how "providers" work. I've used it a few times, but mostly using copy/paste/modify of other examples. Slowly, though, it's starting to become less hazy :D

Reply to this Comment

I wonder if a lighter solution might be to use

$q.when().then(yourFn);

Invoking then should deliver all the exception handling goodness.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.