Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Juan Agustín Moyano
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Juan Agustín Moyano

Aborting An AJAX Request In AngularJS Using httpi

By
Published in Comments (8)

Last week, I demonstrated how to abort an AJAX (Asynchronous JavaScript and XML) request in AngularJS by using the "timeout" property. While only available as of AngularJS 1.2 (and earlier "unstable" releases), it's an awesome feature and one that I thought would be a nice value-add for httpi, my $resource-inspired lightweight $http wrapper. This morning, I updated httpi to provide an .abort() method in the $http request object. But, I wanted to demonstrate why I thought it was an important addition.

View the httpi project in my GitHub account.

Because AJAX requests are asynchronous, things can get a little hairy when you have several AJAX requests running at the same time. You have no way of knowing the order in which the requests will return. For this reason, being able to explicitly "abort" an unwanted AJAX request can make your life a lot easier.

Consider a simple List-Detail interface in which a user can select an item and view the item's detail. If the detail information has to be retrieved using an asynchronous method (ex, AJAX), the user may be able to click on several list items before the first request is resolved. This may end up leaving your View-Model in an unexpected state.

To demonstrate, I've put together a small app that presents a list of friends. If you click on a friend, the client will make a request to the server to gather the detail information before AngularJS renders it on the page. The caveat, in this case, being that the API will "sleep" all API requests for an unequal amount of time. This allows us to create an "unexpected" order of request resolutions.

To manage this problem, we have to keep track of the currently-executing AJAX request. This way, when we go to make a new AJAX request, we can abort the pending request so that it doesn't end up invoking our data resolution / success handlers.

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

	<title>
		Aborting AJAX Requests In AngularJS Using httpi
	</title>

	<style type="text/css">

		a[ ng-click ] {
			color: red ;
			cursor: pointer ;
			text-decoration: underline ;
		}

	</style>
</head>
<body ng-controller="DemoController">

	<h1>
		Aborting AJAX Requests In AngularJS Using httpi
	</h1>

	<!-- List of friends. -->
	<ul>
		<li ng-repeat="friend in friends">

			<a ng-click="showFriend( friend )">{{ friend.name }}</a>

		</li>
	</ul>

	<!-- Friend detail. -->
	<div ng-if="friend">

		<hr />

		<p>
			<strong>ID</strong>: {{ friend.id }} <br />
			<strong>Name</strong>: {{ friend.name }} <br />
			<strong>Job</strong>: {{ friend.job }} <br />
			<strong>Bio (Bacon Ipsum)</strong>: {{ friend.bio }} <br />
		</p>

	</div>


	<!-- Initialize scripts. -->
	<script type="text/javascript" src="../jquery/jquery-2.1.0.min.js"></script>
	<script type="text/javascript" src="../angularjs/angular-1.2.4.min.js"></script>
	<script type="text/javascript" src="./httpi.js"></script>
	<script type="text/javascript">

		// Define the module for our AngularJS application.
		var app = angular.module( "Demo", [ "httpi" ] );


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I control the main demo.
		app.controller(
			"DemoController",
			function( $scope, friendService ) {

				// I contain the data that we are going to render.
				$scope.friends = [];
				$scope.friend = null;

				// I hold on the most recent request for data for this interface. We need
				// to hold on to this so that we can abort it (when necessary).
				var pendingRequest = null;

				// Initialize the list.
				loadFriends();


				// ---
				// PUBLIC METHODS.
				// ---


				// I get the given friend from the repository.
				$scope.showFriend = function( friend ) {

					// If we have a pending request, it's important to abort it. If we
					// don't, then the asynchronous nature of our data access requests
					// could leave our view-model in an unexpected state (if the
					// requests are resolved in an order that is different from that in
					// which they were initiated).
					if ( pendingRequest ) {

						pendingRequest.abort();

					}

					// Store the primary promise (before invoking .then) since this is
					// the object that has the .abort() method (courtesy of httpi). Once
					// you call .then(), you lose access to .abort().
					pendingRequest = friendService.getFriend( friend.id );

					// Handle data response.
					pendingRequest.then(
						function( newFriend ) {

							$scope.friend = newFriend;

						},
						function( code ) {

							console.warn( "Friend couldn't be loaded." );

						}
					);

				};


				// ---
				// PRIVATE METHODS.
				// ---


				// I get the list of friends from the repository.
				function loadFriends() {

					friendService.getFriends().then(
						function( newFriends ) {

							$scope.friends = newFriends;

						}
					);

				}

			}
		);


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// I provide access to the the friend repository.
		app.service(
			"friendService",
			function( httpi, $q ) {

				// Return the public API.
				return({
					getFriend: getFriend,
					getFriends: getFriends
				});


				// ---
				// PUBLIC METHODS.
				// ---


				// I get the friend with the given ID.
				function getFriend( id ) {

					var request = httpi({
						method: "get",
						url: "./api/friend.cfm",
						params: {
							id: id
						}
					});

					return( prepareResponse( request ) );

				}


				// I get the list of friends from the remote server.
				function getFriends() {

					var request = httpi({
						method: "get",
						url: "./api/friends.cfm"
					});

					return( prepareResponse( request ) );

				}


				// ---
				// PRIVATE METHODS.
				// ---


				// Prepare the raw httpi response for use in the calling context. The
				// goal here is two-fold: to encapsulate the underlying AJAX
				// transportation mechanism; but, more importantly, to wire the "abort"
				// method, provided by httpi service into the promise that we return to
				// the calling context.
				function prepareResponse( request ) {

					// "Unwrap" the AJAX response.
					var promise = request.then(
						function handleResolve( response ) {

							return( response.data );

						},
						function handleReject( response ) {

							return( $q.reject( response.status ) );

						}
					);

					// Wire in the underlying abort method.
					promise.abort = request.abort;

					// No matter what happens with the request, free up the object
					// references in order to help the garbage collection.
					promise.finally(
						function handleFinally() {

							request = promise = null;

						}
					);

					return( promise );

				}

			}
		);

	</script>

</body>
</html>

As you can see, whenever we initiate a request, we first call .abort() on the current request. This will make sure the current request is "rejected" and the new AJAX request is the only one running.

If I were to comment-out the .abort() call, we could end up with a page that looks like this:

Aborting AJAX requests in AngularJS can be important or your View-Model may be left in an unexpected state.

Notice that our UI is rendering "Tricia" (ID:3) even though "Kim" (ID:1) was the last request made. This is because the request for (ID:1) resolved before the request for (ID:3) even though the request for (ID:3) was initiated first. Had we aborted the first request when we made the second request, our UI would be rendered properly.

If you're on an earlier version of AngularJS, before the timeout property could be used to abort the underlying AJAX request, you can still manage this problem, just not as easily. Solutions range from the not great - using something like ng-if / ui-if to completely destroy and rebuild the View anytime an ID changes - to the merely inconvenient - checking the response data against the current View-Model to make sure they are in alignment. It works, but it's a headache.

In addition to avoiding race conditions in asynchronous requests, aborting the underlying request can be helpful when your current $scope is destroyed. I've talked about this before, in terms of canceling a $timeout callback, but same applies to AJAX requests; when your $scope is destroyed, you should abort any relevant AJAX request or the eventual request resolution (or rejection) may end up generating an unexpected user experience.

All to say, I've added an .abort() method to my httpi AngularJS module.

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

Reader Comments

96 Comments

@Ben,

I'm seeing a few items here that are quite interesting. Though I'm not all too familiar with Angular.js ... I can't help but notice references to DAO repositories, explicit GC and future promises... concepts I became acquainted working with Spring and JPA. Seeing these features in JS is pretty awesome...

Looks like I'll be stalking Angular a bit more ...

15,902 Comments

@Edward,

Just so you don't give me too much credit, I may play a little fast and loose with my terminology. While JavaScript is a garbage-collected language, I am don't really know too much about how the GC works. From what I have read, it seems there are a few situations in which you can help the GC out, specifically when it comes to the closures that are created when you pass a function "out of scope." In the situation in this post, once the requests are completed, you can safely nul-out references that will no longer be needed.... which I **think** helps the GC figure out which are no longer needed.

BUT, please take that with a grain of salt. I'm not entirely sure if my explanation is accurate.

And as far as "repository", I'm somewhat generically using that to mean "the place where I get data". I refer to it as a repository since I'm trying to detach the concept of AJAX from the actual data, hence the "unwrapping" of the AJAX response before data is passed back to the controller.

Of course, all of this to say that JavaScript can use all kinds of design patterns that apply to other languages.

96 Comments

@Ben ...

I was perusing the Mozilla docs for Mark-and-Sweep GC after reading your entry and it seems apropos to null out resource intensive objects... I was recently working on a legacy enterprise app using JMS 1.1 where the connection factories were explicitly nulled out in a finally block... But JMS connections, like DB connections, are expensive operations... I've never really used memory management strategies with JavaScript... Off to research... :)

A cursory search in Google for {angularjs repository pattern} landed me to an example on StackOverflow that demo'd an Angular implementation of a RESTful controller wired up to a CRUD repository ....

I've taken quite a liking to JPA Repositories ... specifically using Spring Data... It makes dealing with Hibernate fun again...

15,902 Comments

@Edward,

Getting off-topic now, but the concept of repositories (in the stricter sense of the word) is something that I find fascinating and confusing. Like many things, I can "get" it if the object being persisted is a "single thing." And by that I mean it likely maps to a single database record (or a single INNER JOIN). But, when the object contains more of a nested value (ex. a Conversation that contains 1..N Comments), then I don't know how to handle that in a repository. This is where my lack of real programming training starts to shine through, I think.

22 Comments

I've been coming hear since 2008 when I first got into CF but now find that since I'm mainly a JS dev now that this is becoming one of my main JS resources. Good work Ben!!

1 Comments

Thanks for this article. It gave me a very good idea. I created a custom interceptor for aborting angular http and jquery ajax requests. If you can take a look and give your valuable suggestions, that would be great.

http://programmerbuddy.blogspot.in/2014/08/how-to-cancel-all-old-xhr-http-ajax.html

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