Using JavaScript Promises with Cordova

JavaScript Promises

JavaScript Promises could make asynchronous programming a bit easier. Most of the APIs of Cordova are asynchronous. So when you want to call one asynchronous API after the other, you have to nest API in the callback functions of the previous API. Many APIs also take error handler as one of the parameter to API function. At some point the whole code could become very difficult to read and maintain.

JS Promises  could make this a bit simpler. Promise is an object, which takes a function callback with two arguments, resolve and reject. Promise represent a value that would be resolved sometime in future, it is is not already resolved or rejected. The promise can be rejected explicitly or when any JavaScript exception is thrown. See https://promisesaplus.com/ and Promises on MDN for details on JS Promises. Advantages of Promises is realized when you need to chain multiple asynchronous calls. Promises can also be useful if you want to guarantee then a piece of code is executed only once. Promises are executed only once.

Before we see how Promises can simplify usage of Cordova APIs, let’s see a simple example of Promises. Let’s say there are three asynchronous functions and they need to be called one after the other.

<!DOCTYPE html>
<html>
	<head>
		<title>JavaScript Promises Test</title>
	</head>
	<body>
		<script>
			function asyncFunc1(throwError, successCallback, errorCallback) {
				window.setTimeout(function(){
					console.log("Executed asyncFunc1");
					if (throwError)
						errorCallback({error:'Error from asyncFunc1'});
					else
						successCallback({retValue : 'asyncFunc1 return value'});
				}, 1000);
			}

			function asyncFunc2(throwError, successCallback, errorCallback) {
				window.setTimeout(function(){
					console.log("Executed asyncFunc2");
					if (throwError)
						errorCallback({error:'Error from asyncFunc2'});
					else
						successCallback({retValue : 'asyncFunc2 return value'});
				}, 1000);
			}

			function asyncFunc3(throwError, successCallback, errorCallback) {
				window.setTimeout(function(){
					console.log("Executed asyncFunc3");
					if (throwError)
						errorCallback({error:'Error from asyncFunc3'});
					else
						successCallback({retValue : 'asyncFunc3 return value'});
				}, 1000);
			}

			function genericErrorHandler (err) {
				console.log("Error : ", err);
			}
		</script>

	</body>
</html>

Without using Promises, you would call the functions as follows –

asyncFunc1(false, function(ret1){
 	asyncFunc2(false, function(ret2){
 		asyncFunc3(false, function(ret3){
 			console.log(ret3);
 		}, genericErrorHandler);
 	}, genericErrorHandler);
}, genericErrorHandler);

Notice nesting of function calls in the callback of previous call. This may not look too complicated in this example, but if you have large block of code in callback functions then it might become difficult to read and understand the code.

Now let’s see how we can use JS Promises to call the same three async functions. We will use PromiseJS polyfill. Include following script –

<script src="https://www.promisejs.org/polyfills/promise-7.0.4.min.js"></script>

Here is the code to call async functions sequentially using JS Promises –

new Promise(function(resolve, reject){
	asyncFunc1(false, resolve, reject);
}).then (function(ret1) {
	return new Promise(function(resolve,reject){
		asyncFunc2(false, resolve, reject);
	});
}).then (function(ret2){
	return new Promise(function(resolve,reject){
		asyncFunc3(false, resolve, reject);
	});
}).then(function(ret3) {
	console.log(ret3);
}, genericErrorHandler);

Notice that the error handler is handled in the last ‘then’ call. Any error occurred in any of the chained promises would be caught by this error handler. A couple of other notes about Promises –

  1. As mentioned earlier, Promise is executed only once. However, you can call ‘then’ method multiple times. If the promise is already executed then appropriate callback function passed in the ‘then’ would be called – i.e. if promise was executed with success then resolve callback would be called and if error had occurred then reject callback method would be called. ‘then’ takes two arguments – the first one is success callback and the second one is error callback.
  2. If you return a value from ‘then’ method that is a Promise object then the next ‘then’ method is called only after the Promise is executed. If the value returned from ‘then’ is not a Promise, the next ‘then’ method is called immediately
  3. Value passed to resolved callback method (first argument to ‘then’ method) is either the value with which earlier Promise was resolved or value returned directly from the previous Promise/’then’ method

Cordova Example

Let’s take an example from Cordova APIs. Let’s say we want to take a picture using camera, and copy it to applications data directory. Following async calls are involved in this example

  1. Call Camera API to take picture. It will return path of the image file (which would be in temp cache folder of the application)
  2. Resolve the path to get FileEntry object
  3. Resolve path to application’s data directory
  4. Copy the image file to application’s data directory

Here is the code snippet without using Promises –

//Get picture from Camera and save to application's data folder.
//success callback returns path of the copied image file 
function getPictureWithoutPromises(successCallback, errorCallback) {
	//Call Camera API to get Picture
	navigator.camera.getPicture(function (imagePath) {
		//resolve the image path, to get FileEntry object
		window.resolveLocalFileSystemURL(imagePath, function (imageFileEntry) {
			//now get path to applications data folder
			window.resolveLocalFileSystemURL(cordova.file.dataDirectory, function (appDataDirEntry) {
				//copy image file to the data directory
				imageFileEntry.copyTo(appDataDirEntry, null, function (newImageFileEntry) {
					//call success callback handler
					successCallback(newImageFileEntry);
				}, errorCallback);
			}, errorCallback);
		}, errorCallback);
	}, errorCallback);
}

And this is how you would call this function –

getPictureWithoutPromises(function (imageFileEntry) {
	//set src of an <img> tag, assuming we got reference to <img> DOM element before
	image.src = imageFileEntry.toURL();
}, function (err) {
	console.log("Error : ", err);
});

The same could be written using Promises as follows –

//Get picture from Camera and save to application's data folder.
//Returns a Promise, whose resolve method returns path to the copied image file 
function getPictureWithPromises() {
	var sourceImageFileEntry;

	//return a Promise
	return new Promise(function (returnResolve, returnReject) {
		new Promise(function (resolve, reject) {
			//Call Camera API to get Picture				
			navigator.camera.getPicture(resolve, reject);
		}).then(function (imagePath) {
			return new Promise(function (resolve, reject) {
				//resolve the image path, to get FileEntry object
				window.resolveLocalFileSystemURL(imagePath, resolve, reject);
			});
		}).then(function (imageFileEntry) {
			//save this file entry in a local variable, this will be used when copying the file	
			sourceImageFileEntry = imageFileEntry;
			return new Promise(function (resolve, reject) {
				//now get path to applications data folder
				window.resolveLocalFileSystemURL(cordova.file.dataDirectory, resolve, reject);
			});
		}).then(function (appDataDirEntry) {
			return new Promise(function (resolve, reject) {
				//copy image file to the data directory
				sourceImageFileEntry.copyTo(appDataDirEntry, null, resolve, reject);
			});
		}).then(function (newImageFileEntry) {
				//resolve the first Promise (which is returned form this method)	 
				returnResolve(newImageFileEntry)
		});
	});
}

And can be called as follows –

getPictureWithPromises().then(function (imageFileEntry) {
	image.src = imageFileEntry.toURL();
}, function (err) {
	console.log("Error : ", err);
});

Promises do not completely remove nested callbacks, but you can limit them to one or max two level.

-Ram Kulkarni

Leave a Reply