In my previous blog post I promised (pun intended) to walk you through how to implement JavaScript Promises within Lightning Components and discuss how they can help simplify managing asynchronous callbacks.

Browser support

Before we continue we need to cover a little about browser support. Most modern browsers support promises natively, but older browsers may not support the functionality out-of-the-box.

Not sure about browser support? Use one of my favourite websites for determining browser support to get a detailed overview of which browsers support Promises.

Screenshot from caniuse.com showing browser support for JavaScript Promises.

Luckily for us however, Salesforce will polyfill browsers which do not support Promises natively. This means browsers such as Internet Explorer can be supported without any additional work from us. Nice.

Below is an excerpt from the documentation which clears this up for us:

If the browser doesn’t provide a native version, the framework uses a polyfill so that promises work in all browsers supported for Lightning Experience.

First, callback functions

The good

Before we can cover what Promises are we need to talk about callback functions. Talking about callback functions helps us to define why we would want to use Promises in the first place.

Let’s first go into a small example. 

We’re going to define a piece of code which opens a modal window. This isn’t anything fancy, we’re just making something visible to the user in the user interface. 

function openModal(callback){
   // Show the modal
   document.getElementById('modal').style.display = 'block';

   // Other async logic...

   // Execute the callback function
   callback();
}

openModal(function(){
   alert('Executed after the modal has opened!');
});

Now you’ve probably noticed that we’re calling the above openModal function and passing in a function as an argument. This is a callback function. After the modal window has been shown to the user the callback function is executed and the alert is displayed to the user.

The point is that we might not know when the callback function is called. Perhaps there is some asynchronous code which needs to run before callback function code should be executed. Their use provides us with the following benefits:

  • Reusable code.
  • Callback functions are executed after a function completes.
  • That means callback functions can be executed either immediately or after an asynchronous event has occurred. 

This is great! This allows us to reuse this function and run any code after the modal has opened. Just call the function and we’re done!

The bad & the ugly

Callback functions do have their place and can be effectively utilised within projects, but they do have some limits and frustrating parts in their implementation.

Let’s pick up on one of the first issues with callback functions. Communicating errors.

function openModal(success, fail){
   try {
      // Attempt to display the modal window
      success();
   } catch(e){
      fail();
   }
}

openModal(function(){
   alert('Executed after the modal has opened!');
}, function(){
   alert('Whoops! Something went wrong.');
);

Callback functions do not have a standard approach to communicating errors or successes to the calling function. However, a typical convention exists whereby you pass in two callback functions; one for success and one for the failure scenario.

We’ve only covered a very basic example and already the code is much harder to read and we’re only opening a modal window! Let’s just highlight some of the main issues:

  • Order of the arguments becomes important.
  • Confusing to read the code. 
  • Harder to maintain due to the inability to reorder the arguments.

There is also the issue of callback hell

Promises!

Now we’re clear that callback functions aren’t necessarily the best tool to use within our development toolkit, let’s take a look the same problem but using JavaScript Promises. 

function openModal(){
   return new Promise(function(resolve, reject){
      // Show the modal
      document.getElementById('modal').style.display = 'block';

      try {
         // Run some async code ...
         resolve();
      } catch(e){
         reject();
      }
   });
}

openModal()
   .then(function(){
      alert('Executed after the modal has opened!');
   })
   .catch(function(){
      alert('Whoops! Something went wrong.');
   });

You should notice that the code for calling the openModal window is now much more expressive and clearer. 

When the resolve function is called within the Promise it will cause the function registered within the then phase to be executed. The same principle applies to when the reject function is called, the catch function will be called.

The major difference is that our openModal function now returns a new instance of a Promise. This Promise instance accepts a success and failure callback function, but in a defined manner. It is always the same approach no matter how you use Promises.

Using Promises within Lightning Components

To demonstrate how to use Promises within Lightning Components we’re going to use a small example of loading weather data from an Apex controller and display the results in a component. All of which is asynchronous.

In the example we’re going to create a Lightning Component and in its controller file call a helper function which returns a Promise. Inside the Promise we will make the call to the server to retrieve the weather data and when this is returned it will resolve the Promise and pass along the returned data.

WeatherController.cls

The Apex controller for the Lightning Component is quite simple and isn’t doing anything special. We’re just returning some test data to be displayed to the user.

public class WeatherController {

   @AuraEnabled
   public static List<WeatherInfo&gt; getWeatherInfo(){
      return new List<WeatherInfo&gt;{
         new WeatherInfo('Monday', 'Cloudy'),
         new WeatherInfo('Tuesday', 'Cloudy'),
         new WeatherInfo('Wednesday', 'Cloudy'),
         new WeatherInfo('Thursday', 'Cloudy'),
         new WeatherInfo('Friday', 'Sunny!'),
         new WeatherInfo('Saturday', 'Sunny!'),
         new WeatherInfo('Sunday', 'Sunny!')
      };
   }

   public class WeatherInfo {
      @AuraEnabled
      public String dayName;

      @AuraEnabled
      public String description;

      public WeatherInfo(String dayName, String description){
         this.dayName = dayName;
         this.description = description;
      }
   }

}

Weather.cmp

Just like our Apex controller above, this file isn’t that important and is here to only show the output of the loading of the weather data. However, in summary it’s doing the following:

  • Display a loading message whilst we load the weather data
  • Iterate over each day returned and output the weather description for that day.
  • Display a loading error message in case something went wrong loading the weather data.
<pre class="wp-block-syntaxhighlighter-code brush: xml; notranslate"><aura:component controller="WeatherController"&gt;

   <!-- Attributes --&gt;
   <aura:attribute name="loading" type="Boolean" default="true" /&gt;
   <aura:attribute name="loadingError" type="Boolean" default="false" /&gt;
   <aura:attribute name="weatherData" type="Object[]" /&gt;

   <!-- Handlers --&gt;
   <aura:handler name="init" value="{!this}" action="{!c.doInit}" /&gt;

   <!-- Body --&gt;
   <aura:if isTrue="{!v.loading}"&gt;
      
      <!-- Display a loading message whilst we wait --&gt;
      <p&gt;Loading...</p&gt;

      <aura:set attribute="else"&gt;

         <!-- Weather data loaded, or we're in error --&gt;
         <aura:if isTrue="{!v.loadingError}"&gt;

            <!-- There was an error --&gt;
            <p&gt;Whoops! Something went wrong!</p&gt;

            <aura:set attribute="else"&gt;

               <!-- Display the weather data --&gt;
               <aura:iteration items="{!v.weatherData}" var="day"&gt;
                  <div>{!day.dayName + ' ' + day.description}</div>
               </aura:iteration&gt;
            </aura:set&gt;
         </aura:if&gt;

      </aura:set&gt;
   </aura:if&gt;

</aura:component&gt;</pre>

WeatherController.js

Here is where the example wants to illustrate how to use Promises. When the component initialises it calls the loadWeatherData function within the helper, which returns a Promise.

({
   doInit: function(component, event, helper){
      helper.loadWeatherData(component)
         .then($A.getCallback(function(weatherData){
            component.set('v.weatherData', weatherData);
            component.set('v.loadingError', false);
            component.set('v.loading', false);
         }))
         .catch($A.getCallback(function(){
            component.set('v.loadingError', true);
            component.set('v.loading', false);
         }));
   }
})

If the Promise is resolved then we set the weatherData to the data returned by the Promise and hide the loading message. This will display the weather information returned from the Apex controller to the user.

Or if the Promise was rejected, then the loading message is hidden and a loading error message is displayed to the user.

WeatherHelper.js

The helper function below returns a new Promise to the controller. When the Promise is returned the call is made to the server to retrieve the weather data.

({
   loadWeatherData: function(component){
      return new Promise(function(resolve, reject){
         var action = component.get('c.getWeatherInfo');
         action.setCallback(this, function(response){
            if(response.getState() === 'SUCCESS'){
               var weatherData = response.getReturnValue();
               resolve(weatherData);
            } else {
               reject();
            }
         });
         $A.enqueueAction(action);
      });
   }
})

If the state of the response state is “SUCCESS” then we know we’ve successfully retrieved the weather data and can call the resolve callback function and pass in as an argument the weather data. 

Otherwise, if the response state is in error then we call the reject function. 

Promise.all

One of the other benefits to using Promises is that they define a standard approach of running asynchronous code and this allows us to execute multiple in parallel and wait for all of them to complete in one easy go.

var promise1 = Promise.resolve('hello');
var promise2 = new Promise(function(resolve){
   setTimeout(resolve, 2000, 'world');
});
var promise3 = new Promise(function(resolve){
   setTimeout(resolve, 3000, 'hello world');
});

Promise.all([promise1, promise2, promise3])
   .then(function(results){
      console.log('results', results);
      // ['hello', 'world', 'hello world']
   });

This is really useful! We can now run multiple parallel processes and wait for them all to complete in one step. The Promise.all function simplifies, removes code duplication and hides away a lot of complexity from our code.

You could use this principle to load multiple sets of data asynchronously within Lightning Components and wait for them all to complete, then do something. 

Further reading

If you found this article useful and you would like to further increase your understanding about JavaScript promises I would highly recommend reading the following articles: