Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

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.

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!

I wonder if a lighter solution might be to use

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

Invoking then should deliver all the exception handling goodness.