Monkey-Patching The $q Service Using $provide.decorator() In AngularJS
This morning, I posted a blog post on monkey-patching the $q service to include an .fcall()-inspired method for function execution in a Promise chain. After posting it, Tomasz Stryjewski and Phil DeJarnett both pointed out that my use of the .run() block was a poor choice when it came to service augmentation. Instead, they suggested that I use a decorator in a .config() block so that the service could be augmented before the dependency-injector caches it. I've never used a decorator before, so I wanted to try and modify my previous demo.
Run this demo in my JavaScript Demos project on GitHub.
When an AngularJS application is being bootstrapped, it runs through a configuration phase followed by a run phase. Services aren't available until the run phase, which means that we can use the configuration phase as a place to setup our monkey matching. That said, it's not quite that simple - services aren't available in the configuration phase, so we can't directly affect them at that time.
But, the configuration phase does give us an opportunity to define a service decorator before any other part of the AngularJS application requires that service. The decorator is kind of like an interceptor for service instantiation; when the service is instantiated, it is passed-off to (or "through") the decorator before it is finally cached in the dependency-injection container.
The decorator function can make use of dependency-injection; however, as we are intercepting the instantiation of a service, you can't inject the target service by name. Instead, AngularJS injects the target service as "$delegate", which it provides as a "local" override to the DI's invoke() method.
To see this in action, I've refactored my earlier demo to use a .config() block:
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Monkey-Patching The $q Service Using $provide.decorator() In AngularJS
</title>
</head>
<body ng-controller="AppController">
<h1>
Monkey-Patching The $q Service Using $provide.decorator() 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 .config() block so that we have an opportunity to decorate /
// augment the service before anything else in the application needs access to
// it. The config blocks will run before the .run() blocks ... run.
app.config(
function monkeyPatchQService( $provide ) {
// Register a decorator for the $q service.
// --
// NOTE: Defining decorator below in order to reduce indentation.
$provide.decorator( "$q", decorateQService );
// Our decorator will get called when / if the $q service needs to be
// instantiated in the application. It is made available as the
// "$delegate" reference (made available as an override in the "locals"
// used to .invoke() the method. Other services can be injected by name.
// --
// NOTE: This decorator MUST RETURN the "$q" service, otherwise, $q will
// be undefined within the application.
function decorateQService( $delegate, $exceptionHandler ) {
// Create a "natural" reference to our delegate for use locally.
var $q = $delegate;
// Monkey-patch our fcall() method.
$q.fcall = fcall;
// Return the original delegate as our instance of $q.
return( $q );
// ---
// PUBLIC METHODS.
// ---
// 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 )
function fcall() {
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 ]
});
}
}
} // END: decorateQService.
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// 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>
We're not actually altering the $q service during the configuration phase. Instead, we're simply using the configuration phase as a means to define the decorator before any other part of the application has had a chance to instantiate it. This ensures that the $q service will always have the .fcall() method at the time it is injected.
Thinking about the configuration phase of an AngularJS application is fairly new to me. Granted, I've used the configuration phase in the past to define things like routing and exception handling. But, I don't think I really ever understood how it worked - it was mostly copy/paste on my part. Hopefully, this step is the first in my journey towards truly understanding the how configuration and providers work.
Want to use code from this post? Check out the license.
Reader Comments
Ben, you can release it as a stand alone library by cloning my https://github.com/bahmutov/angular-q-timeout using decorator repo (includes unit tests too), and putting your code in.
@Gleb,
This looks really cool. But, this is a bit embarrassing, I know nothing about Bower or Karma (or testing in general). Maybe I can use this as an opportunity to wrap my head around that stuff. I believe Bower is like NPM, but for the client. Let me see what I can do!
@Ben,
This is very simple yet great work. Publish it out, bower, jasmine on Karma are easy peasy, I am sure you will nail it :)
You should check this out :
https://www.npmjs.com/package/angular-extend-promises
This is exactly how it is done: by extending $q using a decorator.
And this library is quite stable and well tested :)
Why bother with all of the code in the catch block?
`$q.when` catches anything bad that happens asynchronously, Anything that goes wrong in the synchronous code (the code in your `try` block) can be caught and returned in a rejected promise like this: `catch(error) { return $q.reject( error ) );`. No need for `$exceptionHandler`, right?
I must be missing something.
@Mohammad,
Thanks for the encouragement! I'll try to carve out some time to dig into some of this stuff soon!
@Etienne,
That's a cool looking library. On a side-note, I've been hearing nothing but good things about BlueBird as a promise library. For the most part, the $q service has been awesome. The only limitation I've found is the lack of the initial method invocation protection. But, Bluebird does have some really thought-provoking methods. I'll have to dig into it a bit more.
@Ward,
I need the try/catch to catch errors in the initial method invocation. It will only get to "loadSomething". Without the try/catch, if loadSomething() were to thrown an error, then it would never even get into the $q.when() context.
As far as passing off to $exceptionHandler(), I just added that because that is what AngularJS is doing when it processes the .then() callbacks. Just monkey-see, monkey-do.
Thanks, Ben. I had not issue with the first try/catch ... understood the intent there. My question concerned the second try/catch (the one within the first catch).
I see that you are copy something you saw in Angular code. I'm trying to imagine the purpose of it and learn something that I must admit is completely tangential to the point of this blog ... something I know you appreciate being the fan of tangents that you are ;-)
Here is my guess ... see if it makes sense to you.
No matter what you're going to return a rejected promise ... which you could have done directly (as I described).
But you should only have gotten into this catch block if there was an unexpected **programming error** (not a value error) during the preparation of the `$q.when(...)`. That's something worth telling the programmer about ... which is why you want to give the `$exceptionHandler( error )` a chance to shout about it RIGHT NOW.
You wrap that in try/no-catch because it's diagnostic; no biggie if it fails.
Finally, you surface the error a second time through the rejected promise where the application can handle it ... just as it would if the async `$q.when` resolves to a rejected promise.
Whew!
Don't know if that makes sense to you but it seems to make sense to me.
Now ... would I go to these great lengths myself? I suppose I should if I'm a framework author ... as the Angular team surely is.
Cheers and many thanks for your investigative posts!
I described why it is important to start the promises right (and handle any errors via promise mechanism) http://bahmutov.calepin.co/starting-promises.html
Well done, @gleb. That's a good post for folks who want to understand promise error scenarios and how to handle them.
I do miss some of Q.js niceties from $q. The ability to extend $q is a lesson worth learning and I'm happy to learn of libraries that do that.
Incidentally, one advantage of $q (relative to Q.js) is testability. The ability to stack up the $q queue and flush it synchronously under test is great; there is no comparable feature in Q.js. Therefore every test involving Q.js must be an async test. That can be annoying.
@Ward, maybe one could make Q to behave synchronously for the purpose of the unit test. For example I made q-flush for Node (could be extended to browser I guess), https://github.com/bahmutov/q-flush
It adds `Q.deferFlush()` and `Q.flush()` methods that can do things like
<pre>
var result;
Q.deferFlush();
Q(10).then(function (value) {
result = value;
});
Q.flush();
console.log(result); // prints 10
</pre>
But usually I would recommend just using Mocha testing framework that handles promises nicely.
@Ward,
Ah, sorry, I didn't understand the question originally. I am wrapping the call to the $exceptionHandler() in a try/catch since the $exceptionHandler() could throw an error itself. In all likelihood, it will not; but, if the developer has decorated or proxied the $exceptionHandler() service then maybe something could go wrong.
It's was a judgement call; but, you are probably right - if something were to go wrong there - ie, the $exceptionHandler() service is broken - then the develop should know about it *before* it goes into production.
I was just trying to reconcile the difference between my code and the core promise functionality. In the AngularJS code, they call the $exceptionHandler() *after* they reject the promise:
try {
. . . // ....
} catch ( error ) {
. . . obj.reject( error );
. . . $exceptionHandler( error );
}
They can do that because they are dealing with a Deferred value. In their code, if the exceptionHandler() did throw an error, I *think* the promise would still work.
However, in my case, I'm dealing with a *return* value. As such, I can't invoke the $exceptionHandler() after - I have to invoke it before I return my rejected value. As such, I guess I was just nervous about the change in workflow and wanted to take extra care.
But, in retrospect, it was probably overkill and could be returned.
Well you can still do it their way and return the promise if you want:
var promise;
try {
. . . // ...
. . . promise = when(...);
} catch ( error ) {
. . . promise = obj.reject( error );
. . . $exceptionHandler( error );
} finally {
. . . return promise;
}
I think we beat this horse to death ??
@Ward,
Ha ha, damn that horse :) I see what you did there, though. Looks good.