Working with promises

Promises play an important role in version 4 of the ArcGIS API for JavaScript. The better you understand promises, the more equipped you'll be to write cleaner code when working with asynchronous operations in your applications.

What is a promise?

On the most basic level, a promise is a representation of a future value returned from an asynchronous task. When the task executes, the promise allows other processes to run simultaneously, which optimizes app performance and gives users a better experience. In essence, a promise is a value that "promises" to be returned whenever the process completes. This is particularly useful when making multiple network requests where timing and download speeds can be unpredictable.

A promise is always in one of three states: pending, resolved, or rejected. When a promise resolves, it can resolve to a value or another promise as defined in a callback function. When a promise is rejected, it should be handled in an errback function.

Understanding .then()

Promises are commonly used with the .then() method. This is a powerful method that allows you to define the callback and errback functions. The first parameter is always the callback and the second is the errback.

someAsyncFunction.then(callback, errback);

The callback is invoked once the promise resolves and the errback is called if the promise is rejected.

someAsyncFunction().then(function(resolvedVal){
  // This is called when the promise resolves
  console.log(resolvedVal);  // Logs the value the promise resolves to
}, function(error){
  // This function is called when the promise is rejected
  console.error(error);  // Logs the error message
});

Another method for handling errors with rejected promises is with the otherwise function. See Esri Error for more information.

button.on("click", function() {
  esriRequest(url, options).then(function(response) {
    // do something useful
  }).otherwise(function(error){
    console.log("informative error message: ", error);
  });
});

Example: GeometryService.project()

In this example, we'll use the GeometryService to project several point geometries to a new spatial reference. In the documentation for GeometryService.project(), we see that project() returns a promise that resolves to an array of projected geometries.

require([
  "esri/tasks/GeometryService",
  "esri/tasks/support/ProjectParameters", ...
  ], function(GeometryService, ProjectParameters, ... ) {

    // Create a new instance of GeometryService
    var gs = new GeometryService( "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Utilities/Geometry/GeometryServer" );
    // Set up the projection parameters
    var params = new ProjectParameters({
      geometries: [points],
      outSR: outSR,
      transformation = transformation
    });

    // Run the project function
    gs.project(params).then(function(projectedGeoms){
      // The promise resolves to the value stored in projectedGeoms
      console.log("projected points: ", projectedGeoms);
      // Do something with the projected geometries inside the .then() function
    }, function(error){
      // Print error if promise is rejected
      console.error(error);
    });
});

Chaining promises

One of the advantages to using promises is leveraging .then() to chain multiple promises together. This can clean up your code nicely by removing nested callback after nested callback, ultimately making your code easier to read. When you chain more than one promise together, the value of each process defined by .then() is dependent on the resolution of the previous promise. This allows you to sequence blocks of code without having to nest multiple callbacks within each other. It is important to note that each .then() that provides a value to another .then() must do so using the return command.

Example: geometryEngineAsync.geodesicBuffer()

We'll demonstrate chaining by continuing the previous projection example and using multiple .then() functions to perform some basic analysis on our projected points. For a live look at a similar example of chaining promises, see the Chaining Promises sample.


  /***************************************
  * See previous example for preceding code
  ****************************************/

  // Creates instance of GraphicsLayer to store buffer graphics
  var bufferLayer = new GraphicsLayer();

  // Project the points and chain the promise to other functions
  gs.project(params)
    .then(bufferPoints)             // When project() resolves, the points are sent to bufferPoints()
    .then(addGraphicsToBufferLayer) // When bufferPoints() resolves, the buffers are sent to addGraphicsToBufferLayer()
    .then(calculateAreas)           // When addGraphicsToBufferLayer() resolves, the buffer geometries are sent to calculateAreas()
    .then(sumArea)
    .otherwise(function(error){ // When calculateAreas() resolves, the areas are sent to sumArea()
      console.error('One of the promises in the chain was rejected! Message: ', error);
    });

  // Note how each function returns the value used as input for the next function in the chain

  // Buffers each point in the array by 1000 feet
  function bufferPoints(points) {
    return geometryEngine.geodesicBuffer(points, 1000, 'feet');
  }

  // Creates a new graphic for each buffer and adds it to the bufferLayer
  function addGraphicsToBufferLayer(buffers) {
    buffers.forEach(function(buffer) {
      bufferLayer.add(new Graphic(buffer));
    });
    return buffers;
  }

  // Calculates the area of each buffer and adds it to a new array
  function calculateAreas(buffers) {
    return buffers.map(function(buffer) {
      return geometryEngine.geodesicArea(buffer, 'square-feet');
    });
  }

  // Calculates the sum of all the areas in the array returned from the previous then()
  function sumArea(areas) {
    var total = 0;
    areas.forEach(function(area) {
      total += area;
    });
    console.log("Total buffered area = ", total, " square feet.");
  }

API classes as promises

Many methods in the ArcGIS API for JavaScript return promises. For example all methods in the task modules and the GeometryEngineAsync module return promises.

Not only do functions return promises, but several classes in the API are promises when instantiated. This means you can call .then() directly on the instance. For example, since an instance of esri/views/MapView is promise-based, you can call view.then() to execute code in the callback once the view is ready. The following classes are promises once you create an instance of them:

Promises vs. Event listeners

The then() method appears to behave similar to an event listener. So why should you use a promise instead of an event listener? Promises are nice because they allow you to access the result of an asynchronous process directly after it completes. In comparison, if you initialize an event listener after an event has occurred, the listener will never fire.

Example: MapView

In this example, we'll use .then() on an instance of MapView. The code in the callback of view.then() won't execute until the view has loaded. The equivalent of this workflow in the 3.x versions of the API would be creating a map.on('load', callback) event handler that executes the callback once the map loads. For a live look at an example of using view.then(), see the 2D overview map sample.

require(["esri/Map", "esri/views/MapView", "dojo/domReady!"], function(Map, MapView){
  var map = new Map({
    basemap: 'streets'
  });

  var view = new MapView({
    container: "viewDiv",
    scale: 24000,
    map: map
  });

  view.then(function(instance){
    // Do something here when the view finishes loading
  }, function(error){
    console.log('MapView promise rejected! Message: ', error);
  });
});

Additional resources

You can read more about promises in the MDN Promise documentation and the Dojo Promise documentation to get a more in-depth look at their structure and usage. The following are additional links to blogs that explain promises with other helpful examples: