How to use Javascript Promises to lazily update data

Last week I was working on a simple implementation updating a shopping cart for a site, the frontend was written in html/javascript. The brief - when the quantity of an item in the cart was modified the client could press an update cart button which would update the cart database, after which it was necessary to recalculate the total values of the order and refresh the page with the new totals.

Simple solution, just iterate through the items in the cart updating the database and recalculate the totals. Then refresh the page.

Using Javascript and an Array.forEach() method

function updateCart( items ) {
	items.forEach( function( item ) {
		updateToCart( item.id, item.quantity ); // update database
	});
	// calculate the new totals
	// refresh page
}
function updateToCart( id, quantity ) {
    $.ajax( {
            url: "cart/update",
            type: "POST",
            data: JSON.stringify( {"id": id, "quantity": quantity} ),
            success: function ( data, textStatus, jqXHR) {
            console.log( 'Item updated: ' + id + ', ' + textStatus );
        },
        error: function ( jqXHR, textStatus, errorThrown ) {
            console.error( 'Could not update item: ' + id + ', due to: ' + textStatus + ' | ' + errorThrown);
        }
    });
}

Except…

We are using an ajax request with jQuery to update the cart database. The forEach loop will just iterate through and complete, and the remainder of the code will be executed even if the database is still being updated, or even worse has failed to update.

Recalculating the totals could either be incorrect or even worse. This demonstrates exactly when not to use the forEach method on Arrays in javascript.

IN JAVASCRIPT BEWARE USING ARRAY.FOREACH() METHODS WHICH CALL ASYNCHRONOUS FUNCTIONS.

Normally a forEach method in javascript is synchronous. But if the body of the forEach method calls an asynchronous function, then the forEach method can complete before the asynchronous functions have completed.

To avoid this dispense with the forEach method, create a callback function to call the asynchronous function and use another means to terminate the callback and continue with the normal code execution. An if else statement can be used to this effect as a form of control.

If we have some code which requires synchronous execution and has this form of forEach method within it, then remove the forEach method and split the code with an if else test to check when all items have been iterated through. It is probably best to split the code into two functions. The second function will only be called after the callback function completes.

Using Javascript and an Array with an if…else statement

// function updateCart() - replace items.forEach() with callback function next()
function updateCart() {
    var cartsize = items.length;
    var idx = 0;
    next = function() {
        if (idx < cartsize) {
            // call updateToCart function passing the callback function next
            updateToCart(items[idx].id, items[idx].quantity, next);
            idx++; // increment the items counter
        } else {
            // continue execution
            recalcCartTotals();
        }
    }
    next(); // call the callback function to start the iteration
}
function updateToCart( id, quantity, next ) {
    $.ajax( {
        url: "cart/update",
        type: "POST",
        data: JSON.stringify( { "id": id, "quantity": quantity } ),
        success: function ( data, textStatus, jqXHR ) {
            console.log( 'Item updated: ' + id + ', ' + textStatus );
            next();
        },
        error: function ( jqXHR, textStatus, errorThrown ) {
            console.error( 'Could not update item: ' + id + ', due to: ' + textStatus + ' | ' + errorThrown);
            next();
        }
    });
}

More Recent Developments ….

Using Javascript callback functions has always been the accepted way to handle these situations, but there is now a new object in Javascript, the Promise object which has been created to handle asynchronous operations.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

The method used on iterable objects is the Promise.all( iterable object ).

Promises Promises…

The Promise.all() method takes an array of promises and fires one callback once they are all resolved:

To implement the Promise object it requires each call to the asynchronous function to return either resolve or reject callback. If no return callback is made then the Promise object does not trigger any action.

function updateCart() {
	console.log("Updating Cart");
	var promises = [];
	var itemRows = document.getElementById("cart-list").rows;
	for(var i = 0; i < itemRows.length; i++) {
		var id = itemRows[i].cells[2].id;
		var quantity = ItemRows[i].cells[2].getElementsByTagName('input')[0].value;
		var p = new Promise(function(resolve, reject){updateToCart(id, quantity, resolve, reject);});
		promises.push(p);
	}
	Promise.all(promises).then(function() {
		recalcCartTotals();
	});
}
function updateToCart(id, quantity, resolve, reject) {
	console.log("Sending request to update cart: item: " + id + " 	quantity: " + quantity);
	$.ajax({
		url: "cart/update",
		type: "POST",
		data: JSON.stringify({"id": id, "quantity": quantity}),
		success: function (data, textStatus, jqXHR) {
			console.log('Item updated: ' + id + ', ' + textStatus);
			return resolve();
		},
		error: function (jqXHR, textStatus, errorThrown) {
			console.error('Could not update item: ' + id + ', due to: ' + textStatus + ' | ' + errorThrown);
			return reject();
		}
	});
}

Note: it is not necessary to return any value with the resolve function or the reject function, but it will pass details of errors as a parameter if so required.