Monkey-Patching The $q Service With .fcall() In AngularJS
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 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.
Want to use code from this post? Check out the license.
Reader Comments
Ben,
For your demos - just include https://github.com/bahmutov/console-log-div script on your page (you can even do this through https://rawgit.com/bahmutov/console-log-div/master/console-log-div.js) to mirror console.log and console.error calls onto the page. Then you don't need to open browser console to show the results.
Ben,
It would be nice to have this patch as a stand alone bower / npm module, because I want to use it.
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.
@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!
@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!
@Ben,
Decorator pattern is definitely the way to go here ;-)
This is a very good demo Ben. Thanks for sharing.
@Tomasz, @Phil,
Thanks again for the feedback - decorators look pretty cool!
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
@Ben,
Fixed the firefox (textContent property), all set.
@Gleb,
Confirmed fixed on my end as well.
I wonder if a lighter solution might be to use
$q.when().then(yourFn);
Invoking then should deliver all the exception handling goodness.