Skip to main content
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: James Allen
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: James Allen

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

By
Published in Comments (11)

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.

Want to use code from this post? Check out the license.

Reader Comments

19 Comments

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

17 Comments

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.

15,902 Comments

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

15,902 Comments

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

1 Comments

I wonder if a lighter solution might be to use

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

Invoking then should deliver all the exception handling goodness.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel