Partager via


Async programming patterns and tips in Hilo (Windows Store apps using JavaScript and HTML)

[This article is for Windows 8.x and Windows Phone 8.x developers writing Windows Runtime apps. If you’re developing for Windows 10, see the latest documentation]

From: Developing an end-to-end Windows Store app using JavaScript: Hilo

Previous page | Next page

Hilo uses promises to process the results of asynchronous operations. Promises are the required pattern for asynchronous programming in Windows Store apps using JavaScript. Here are some tips and guidance for using promises, and examples of the various ways you can use promises and construct promise chains in your app.

Download

After you download the code, see Getting started with Hilo for instructions.

You will learn

  • How to compose asynchronous code.
  • How to use promises.
  • How to chain and group promises.
  • How to code promise chains to avoid nesting.
  • How to wrap non-promise values in a promise.
  • How to handle errors in a promise.

Applies to

  • Windows Runtime for Windows 8
  • WinJS
  • JavaScript

A brief introduction to promises

To support asynchronous programming in JavaScript, Windows Runtime and the WinJS implement the Common JS Promises/A specification. A promise is an object that represents a value that will be available later. Windows Runtime and WinJS wrap most APIs in a promise object to support asynchronous method calls and the asynchronous programming style.

When using a promise, you can call the then method on the returned promise object to assign the handlers for results or errors. The first parameter passed to then specifies the callback function (or completion handler) to run when the promise completes without errors. Here is a simple example in Hilo, which specifies a function to run when an asynchronous call, stored in queryPromise, completes. In this example, the result of the asynchronous call gets passed into the completion handler, _createViewModels.

Hilo\Hilo\imageQueryBuilder.js

    if (this.settings.bindable) {
        // Create `Hilo.Picture` objects instead of returning `StorageFile` objects
        queryPromise = queryPromise.then(this._createViewModels);
    }

You can create a promise without invoking then immediately. To do this, you can store a reference to the promise and invoke then at a later time. For an example of this, see Grouping a promise.

Because the call to then itself returns a promise, you can use then to construct promise chains, and pass along results to each promise in the chain. The return value may or may not be ignored. For more info on promises and the other methods that promises support, see Asynchronous programming in JavaScript.

[Top]

How to use a promise chain

There are several places in Hilo where we constructed a promise chain to support a series of asynchronous tasks. The code example here shows the TileUpdater.update method. This code controls the process that creates the thumbnails folder, selects the images, and updates the tile. Some of these tasks require the use of asynchronous Windows Runtime functions (such as getThumbnailAsync). Other tasks in this promise chain are synchronous, but we chose to represent all the tasks as promises for consistency.

Hilo\Hilo\Tiles\TileUpdater.js

update: function () {
    // Bind the function to a context, so that `this` will be resolved
    // when it is invoked in the promise.
    var queueTileUpdates = this.queueTileUpdates.bind(this);

    // Build a query to get the number of images needed for the tiles.
    var queryBuilder = new Hilo.ImageQueryBuilder();
    queryBuilder.count(numberOfImagesToRetrieve);

    // What follows is a chain of promises. These outline a number of 
    // asychronous operations that are executed in order. For more 
    // information on how promises work, see the readme.txt in the 
    // root of this project.
    var whenImagesForTileRetrieved = queryBuilder.build(picturesLibrary).execute();
    whenImagesForTileRetrieved
        .then(Hilo.Tiles.createTileFriendlyImages)
        .then(this.getLocalImagePaths)
        .then(Hilo.Tiles.createTileUpdates)
        .then(queueTileUpdates);
}

Each function in the promise chain passes its result as input to the next function. For example, when whenImagesForTileRetrieved completes, it invokes the completion handler, createTileFriendlyImages. The calling function automatically passes the completion handler an array of files returned from the asynchronous function call. In this example the asynchronous function is queryBuilder.build.execute, which returns a promise.

The next diagram shows the operation flow in the tile updater’s promise chain. This flow creates thumbnail images and tile updates. Creating a thumbnail from a picture file requires asynchronous steps that don’t follow a straight-line. In the diagram, the solid ovals are asynchronous operations in the Windows Runtime. The dashed ovals are tasks that call synchronous functions. The arrows are inputs and outputs.

For more information about promise chains, see Chaining promises.

[Top]

Using the bind function

The JavaScript bind function creates a new function with the same body as the original function, in which the this keyword resolves to the first parameter passed into bind. You can also pass additional parameters to the new function in the call to bind. For more info, see the bind function.

In coding asynchronous operations in Hilo, the bind function helped us with a couple of scenarios. In the first scenario, we used bind to preserve the evaluation of this in the execution context, and pass along local variables to the closure (a typical use in JavaScript). In the second scenario, we wanted to pass multiple parameters to the completion handler.

The code in TileUpdater.js provides an example of the first scenario where bind was useful. The last completion handler in the tile updater’s promise chain, queueTileUpdates, gets bound in the tile updater’s update method (shown previously). Here, we create a bound version of queueTileUpdates by using the this keyword, at which point this contains a reference to the TileUpdater object.

Hilo\Hilo\Tiles\TileUpdater.js

var queueTileUpdates = this.queueTileUpdates.bind(this);

By using bind, we preserve the value of this for use in the forEach loop in queueTileUpdates. Without binding the TileUpdater object, which we later assign to a local variable (var self = this), we would get an exception for an undefined value if we tried to call this.tileUpdater.update(notification) in the forEach loop.

Hilo\Hilo\Tiles\TileUpdater.js

queueTileUpdates: function (notifications) {
    var self = this;
    notifications.forEach(function (notification) {
        self.tileUpdater.update(notification);
    });
},

The second scenario where bind is useful, for passing multiple parameters, is shown in the tile updater’s promise chain. In the first completion handler in the tile updater’s promise chain, createTileFriendlyImages, we use bind to partially apply two functions. For more info on partial function application, see Partial application and this post. In Hilo, the copyFilesToFolder and returnFileNamesFor functions are bound to the context (null, in this case) and to the array of files as a parameter. For copyFilesToFolder, we wanted to pass in two arguments: the array of files (thumbnail images), and the result from createFolderAsync, which is the target folder for the thumbnails.

Hilo\Hilo\Tiles\createTileFriendlyImages.js

function createTileFriendlyImages(files) {
    var localFolder = applicationData.current.localFolder;

    // We utilize the concept of [Partial Application][1], specifically
    // using the [`bind`][2] method available on functions in JavaScript.
    // `bind` allows us to take an existing function and to create a new 
    // one with arguments that been already been supplied (or rather
    // "applied") ahead of time.
    //
    // [1]: http://en.wikipedia.org/wiki/Partial_application 
    // [2]: https://msdn.microsoft.com/en-us/library/windows/apps/ff841995

    // Partially apply `copyFilesToFolder` to carry the files parameter with it,
    // allowing it to be used as a promise/callback that only needs to have
    // the `targetFolder` parameter supplied.
    var copyThumbnailsToFolder = copyFilesToFolder.bind(null, files);

    // Promise to build the thumbnails and return the list of local file paths.
    var whenFolderCreated = localFolder.createFolderAsync(thumbnailFolderName, creationCollisionOption.replaceExisting);

    return whenFolderCreated
        .then(copyThumbnailsToFolder);
}

Tip  We can bind to null because the bound functions do not use the this keyword.

 

The bound version of copyFilesToFolder is assigned to copyThumbnailsToFolder. When copyFilesToFolder is invoked, the previously-bound array of files is passed in as the first input parameter, sourceFiles. The target folder, which was stored in the previous code example as the result from createFolderAsync, is passed into copyFilesToFolder automatically as the second parameter, instead of the only parameter.

Hilo\Hilo\Tiles\createTileFriendlyImages.js

function copyFilesToFolder(sourceFiles, targetFolder) {

    var allFilesCopied = sourceFiles.map(function (fileInfo, index) {
        // Create a new file in the target folder for each 
        // file in `sourceFiles`.
        var thumbnailFileName = index + ".jpg";
        var copyThumbnailToFile = writeThumbnailToFile.bind(this, fileInfo);
        var whenFileCreated = targetFolder.createFileAsync(thumbnailFileName, creationCollisionOption.replaceExisting);

        return whenFileCreated
            .then(copyThumbnailToFile)
            .then(function () { return thumbnailFileName; });
    });

    // We now want to wait until all of the files are finished 
    // copying. We can "join" the file copy promises into 
    // a single promise that is returned from this method.
    return WinJS.Promise.join(allFilesCopied);
};

[Top]

Grouping a promise

When you have non-sequential, asynchronous operations that must all complete before you can continue a task, you can use the WinJS.Promise.join method to group the promises. The result of Promise.join is itself a promise. This promise completes successfully when all the joined promises complete successfully. Otherwise, the promise returns in an error state.

The following code copies tile images to a new folder. To do this, we wait until we have opened a new output stream (whenFileIsOpen) and we have obtained the input file that contains a tile image (whenThumbnailIsReady). Then we start the task of actually copying the image. We pass in the two returned promise objects to the join function.

Hilo\Hilo\Tiles\createTileFriendlyImages.js

var whenFileIsOpen = targetFile.openAsync(fileAccessMode.readWrite);
var whenThumbailIsReady = sourceFile.getThumbnailAsync(thumbnailMode.singleItem);
var whenEverythingIsReady = WinJS.Promise.join({ opened: whenFileIsOpen, ready: whenThumbailIsReady });

When you use join, the return values from the joined promises are passed as input to the completion handler. Continuing with the tiles example, args.opened, below, contains the return value from whenFileIsOpen, and args.ready contains the return value from whenThumbnailIsReady.

Hilo\Hilo\Tiles\createTileFriendlyImages.js

whenEverythingIsReady.then(function (args) {
    // `args` contains the output from both `whenFileIsOpen` and `whenThumbailIsReady`.
    // We can identify them by the order they were in when we joined them.
    outputStream = args.opened;
    var thumbnail = args.ready;

[Top]

Nesting in a promise chain

In earlier iterations of Hilo, we created promise chains by invoking then inside the completion handler for the preceding promise. This resulted in deeply nested chains that were difficult to read. Here is an early version of the writeThumbnailToFile function.

   function writeThumbnailToFile(fileInfo, thumbnailFile) {   
       var whenFileIsOpen = thumbnailFile.openAsync(readWrite);   
       
       return whenFileIsOpen.then(function (outputStream) {   
       
           return fileInfo.getThumbnailAsync(thumbnailMode).then(function (thumbnail) {   
               var inputStream = thumbnail.getInputStreamAt(0);   
               return randomAccessStream.copyAsync(inputStream, outputStream).then(function () {   
                   return outputStream.flushAsync().then(function () {   
                       inputStream.close();   
                       outputStream.close();   
                       return fileInfo.name;   
                   });   
               });   
           });   
       });   
   }

We improved code like this by calling then directly on each returned promise instead in the completion handler for the preceding promise. For a more detailed explanation, see post. The following code is semantically equivalent to the preceding code, apart from the fact that we also used join to group promises, but we found it easier to read.

Hilo\Hilo\Tiles\createTileFriendlyImages.js

function writeThumbnailToFile(sourceFile, targetFile) {
    var whenFileIsOpen = targetFile.openAsync(fileAccessMode.readWrite);
    var whenThumbailIsReady = sourceFile.getThumbnailAsync(thumbnailMode.singleItem);
    var whenEverythingIsReady = WinJS.Promise.join({ opened: whenFileIsOpen, ready: whenThumbailIsReady });

    var inputStream,
        outputStream;

    whenEverythingIsReady.then(function (args) {
        // `args` contains the output from both `whenFileIsOpen` and `whenThumbailIsReady`.
        // We can identify them by the order they were in when we joined them.
        outputStream = args.opened;
        var thumbnail = args.ready;
        inputStream = thumbnail.getInputStreamAt(0);
        return randomAccessStream.copyAsync(inputStream, outputStream);

    }).then(function () {
        return outputStream.flushAsync();

    }).done(function () {
        inputStream.close();
        outputStream.close();
    });
}

In the preceding code, we need to include an error handling function as a best practice to make sure that the input and output streams are closed when the function returns. For more info, see Handling errors.

It is worth noting that in the first implementation, we passed along some values via the closure (e.g., inputStream and outputStream). In the second, we had to declare them in the outer scope because it was the only common closure.

[Top]

Wrapping values in a promise

When you create your own objects that make asynchronous calls, you may need to explicitly wrap non-promise values in a promise using Promise.as. For example, in the query builder, we wrap the return value from Hilo.Picture.from in a promise because the from function ends up calling several other asynchronous Windows Runtime methods (getThumbnailAsync and retrievePropertiesAsync).

Hilo\Hilo\imageQueryBuilder.js

_createViewModels: function (files) {
    return WinJS.Promise.as(files.map(Hilo.Picture.from));
}

For more info on Hilo.Picture objects, see Using the query builder pattern.

[Top]

Handling errors

When there’s an error in a completion handler, the then function returns a promise in an error state. If you don’t have an error handler in the promise chain, you may not see the error. To avoid this situation, the best practice is to include an error handler in the last clause of the promise chain. The error handler will pick up any errors that happen along the line.

Important  The use of done is recommended at the end of a promise chain instead of then. They are syntactically the same, but with then you can choose to continue the chain. done does not return another promise, so the chain cannot be continued. Use done instead of then at the end of a promise chain to make sure that unhandled exceptions are thrown. For more info, see How to handle errors with promises.

 

The following code example shows the use of done in a promise chain.

Hilo\Hilo\month\monthPresenter.js

return this._getMonthFoldersFor(targetFolder)
    .then(this._queryImagesPerMonth)
    .then(this._buildViewModelsForMonths)
    .then(this._createDataSources)
    .then(function (dataSources) {
        self._setupListViews(dataSources.images, dataSources.years);
        self.loadingIndicatorEl.style.display = "none";
        self.selectLayout();
    });

In Hilo, we implement an error handling function when attempting to save a cropped image on the crop page. The error handling function must be the second parameter passed to either then or done. In this code, the error handling function corrects the photo orientation, if an EXIF orientation is returned but not supported.

Hilo\Hilo\crop\croppedImageWriter.js

var decoderPromise = getOrientation
    .then(function (retrievedProps) {

        // Even though the EXIF properties were returned, 
        // they still might not include the `System.Photo.Orientation`.
        // In that case, we will assume that the image is not rotated.
        exifOrientation = (retrievedProps.size !== 0)
            ? retrievedProps["System.Photo.Orientation"]
            : photoOrientation.normal;

    }, function (error) {
        // The file format does not support EXIF properties, continue 
        // without applying EXIF orientation.
        switch (error.number) {
            case Hilo.ImageWriter.WINCODEC_ERR_UNSUPPORTEDOPERATION:
            case Hilo.ImageWriter.WINCODEC_ERR_PROPERTYNOTSUPPORTED:
                // The image does not support EXIF orientation, so
                // set it to normal. this allows the getRotatedBounds
                // to work propertly.
                exifOrientation = photoOrientation.normal;
                break;
            default:
                throw error;
        }
    });

For info on best practices related to unit testing and error handling for asynchronous calls, see Testing and deploying the app.

[Top]