Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Nick Floyd
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Nick Floyd

Handling Top-Level Errors In A Promise Workflow In AngularJS

By
Published in Comments (7)

In yesterday's post, on asynchronous promise workflows in AngularJS, all of my state transformation was done with explicit return values. As such, you didn't see the fact that the .then() method implicitly handles exceptions by transforming them into a rejected state. But, this feature got me thinking - what about exceptions raised during the pre-promise workflow? Meaning, what if the method that returns the first promise raises an exception?

Run this demo in my JavaScript Demos project on GitHub.

The exception handling in a promise chain only protects us once we have the initial promise object. As such, exceptions raised during the generation of the initial promise will result in an uncaught exception. To see this in action, take a look at the following code:

NOTE: In AngularJS, there are actually very few truly "uncaught" exceptions. Almost every touch-point in an AngularJS application is implicitly wrapped in a try/catch block and managed by AngularJS' exception handling workflow.

<!doctype html>
<html ng-app="Demo">
<head>
	<meta charset="utf-8" />

	<title>
		Handling Top-Level Errors In A Promise Workflow In AngularJS
	</title>
</head>
<body ng-controller="AppController">

	<h1>
		Handling Top-Level Errors In A Promise Workflow In AngularJS
	</h1>

	<p>
		The naked version.
	</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 control the root of the application.
		app.controller(
			"AppController",
			function( $q ) {

				// In this version, our top-level request for the promise is naked and
				// exposed. While it does return a promise in its happy-path, it raises
				// an exception if invoked during some unexpected state.
				loadSomething()
					.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() {

					// If the state of the service is such that now is not a good time to
					// actually invoke the service, raises exception.
					if ( this.someStateFlag !== 1 ) {

						throw( new Error( "InvalidState" ) );

					}

					return( $q.when( "someValue" ) );

				}

			}
		);

	</script>

</body>
</html>

As you can see, the loadSomething() will return a promise in its "happy path"; but, it will throw an error if it's called while in some inappropriate state. And, when we run the above code, we get the following console output:

Error: InvalidState

Notice that this was handled by the AngularJS exception handler, not our promise-based rejection handler (otherwise, we'd see "Rejected!" in the output).

Now, there may be some philosophical debate as to whether or not the root service, that we're calling, should ever throw an error. Meaning, if a method is supposed to return a promise, is it valid to ever throw an error? Or, should it return a "rejected" promise with the error object as the "reason" for failure?

According to this excellent discussion about Promises from Domenic Denicola, exceptions are a natural part of the promise workflow. But, he primarily talks about throwing errors inside of promise callbacks; he doesn't talk about handling exceptions within methods that should return promises.

As a thought-experiment, assume that it's valid for a promise generating function to throw an error (ie, that this is not a bug). If that's the case, an easy fix would be to make sure that the promise generator is, itself, invoked within a promise chain. In AngularJS, this is quite easy to do - simply use the $q.when() method to start the workflow with an already-resolved promise:

<!doctype html>
<html ng-app="Demo">
<head>
	<meta charset="utf-8" />

	<title>
		Handling Top-Level Errors In A Promise Workflow In AngularJS
	</title>
</head>
<body ng-controller="AppController">

	<h1>
		Handling Top-Level Errors In A Promise Workflow In AngularJS
	</h1>

	<p>
		The when-wrapped version.
	</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 control the root of the application.
		app.controller(
			"AppController",
			function( $q ) {

				// In this version, our top-level request is executed inside of the
				// resolution handler for an implicitly resolved promise chain. As such,
				// it is executed in the context of an error handler which will implicitly
				// transform the promise flow into a rejected state.
				$q.when()
					.then(
						function() {

							// Implicitly protected by the then-container.
							return( loadSomething() );

						}
					)
					.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() {

					// If the state of the service is such that now is not a good time to
					// actually invoke the service, raises exception.
					if ( this.someStateFlag !== 1 ) {

						throw( new Error( "InvalidState" ) );

					}

					return( $q.when( "someValue" ) );

				}

			}
		);

	</script>

</body>
</html>

Here, we start the workflow with an already-resolved promise. Our first resolution handler then invokes the original top-level function which may or may not throw an error. This time, however, since it is protected by the promise workflow, any error will result in a rejected state for the next .then() callback - not an exception.

And, in fact, when we run the above code, we get the following output:

Handling top-level errors in a promise-based workflow in AngularJS.

The first log item there is a result of the error being handed off to AngularJS' exception management (part of the internal $q service workflow). The second and third log items, however, are our from our rejection handler. As you can see, the error thrown by our service was cause by the promise workflow and resulted in a rejected state, not an exception.

Again, there might be some concern about whether or not our original promise-generator should ever throw an error (in lieu of return a rejected promise). But, if you look at the Q library, it seems that they provide the Q.fcall() method for this very reason; which, makes me think, maybe it is valid. I'll need to do some more noodling on this; and if anyone has any strong feelings on the topic, I'd love to hear them.

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

Reader Comments

19 Comments

It is important to wrap the initial call and catch any errors there and direct them into `.catch` callback. There is Q.fcall, I prefer to use an alias to it `Q.try ` https://github.com/kriskowal/q/wiki/API-Reference#promisefcallargs

I usually start my promise chains (I should write a blog post about it) using Q.try, for example see my changing working directory utility chdir-promise https://github.com/bahmutov/chdir-promise/blob/master/index.js - the module exports

function _to(folderName)

wrapped in

Q.try

block (using spots https://github.com/bahmutov/spots to wait for actual folder name).

19 Comments

@Gleb,

Sorry, the code got abridged in the above comment

var S = require('spots');
// _to does NOT return a promise
function _to(folderName) {
}
// exports a method to that returns a promise and catches any error in _to
module.exports = {
to: S(q.try, _to, S)
};
15,811 Comments

@Gleb,

I love the idea of being able to simplify the invocation with something like fcall() or Spot. Having to create the promise and then a parent .then() handler seems like too much cruft (especially with all whitespace my OCD requires me to put in the code).

That said, it would be quite easy to monkey-patch the $q service when it loads so that it can have an .fcall() method. Hmmmm, me thinks there's a blog post in there ;)

1 Comments

I've started experimenting with this just this week, but I am having problems with my unit tests.
I have a controller with a $http call. This ofcourse returns a promise, which I handle with a 'then' and a 'catch'. The catch catches both the normal errors (like Status 500), and the 'throw new Error()' I do in the 'then' section in case of certain errors.
In the unit tests I somehow cannot get the expect to expect an error, and the unit test fails. Do you have any experience with this?

15,811 Comments

@Maarten,

Unfortunately, I know next to nothing about testing :( That's something I am working on. That said, Promise methods are typically asynchronous (at least, they are supposed to be). As such, you might have to tell your test runner to wait for the promise to "come back". Or, maybe the test runner passes a callback, or something, to execute when your asynchronous process has come back.

Forgive me for the vague and bumbling answer; I just don't know much about testing. But, I do know that they usually provide some sort of mechanism to pause the test while asynchronous work is being done.

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