Skip to Content
Technical Articles
Author's profile photo Mike Doyle

Call me back when everything’s done…How Promise.all makes life easy

The Promise.all function allows you to fire off a number of asynchronous calls and then write some logic to run when they’re all done.  If you haven’t tried it you should.

 

What’s needed?

My customer wants a UI5 form with a checklist.  The checklist must be dynamically generated.  The sections, questions and possible answers will all be maintained as Custom Business Object entries in S/4HANA Cloud.  There’s a hierarchy, with each section containing one or many questions.  Each question has one or many answers, often (but not always) Yes or No.  They want to be able to add a new question without needing to change the code.

The app will run on the Cloud Foundry environment of SAP Cloud Platform, and I’m using a combination of the SAP Cloud Application Programming Model (APM) and the SAP S/4HANA Cloud SDK (S4SDK) to combine various SAP-provided APIs, Custom Business Objects and custom CDS views into a single OData service that the UI5 app can consume.

What’s the tricky part?

If the back-end was accessed via a Netweaver Gateway server I would probably write the OData service to support a ‘double expand’.  The url would look like this:

……/ChecklistSections?$expand=toQuestions/toAnswers

That would give me the whole hierarchy in one go (Sections->Questions->Answers), and I could then bind the UI controls to the OData model.

Sadly, this kind of request isn’t supported yet by the APM/S4SDK combination that I’m using.  It looks like I could expand on a single section to get the questions, say, but not to get the whole lot in one go.  If it is possible it isn’t documented!

What I can do is to retrieve the 3 entities independently.  That is to say I can make a call to get all the sections, another to get all the questions and then finally all the possible answers to the questions.  Then I can build the hierarchy using JavaScript (in the UI5 app).

What’s the anti-pattern?

In the scenario I’ve described above we want to build the hierarchy but we can only do it when we have sections, questions and answers.  We don’t care which order they come back in, but we need all of them to be available.

In the success callback of each OData read we could set a flag to say ‘we’re done’.  Then we could check the flags of the other calls, to see if they are done too.  Only on the final callback (whichever one that happens to be) will we proceed to build the hierarchy.   I don’t like this pattern because it’s messy and bug-prone.

We could run the calls in series instead of in parallel (i.e. sections callback triggers the read of questions) but obviously this will be much slower.

Promises.all to the rescue!

If you’re familiar with passing success and error callback functions (e.g. with the v2.OdataModel read method) then you know the ‘old way’ of doing asynchronous calls.  With promises (the ‘new way’) you don’t pass the callback functions when you make the asynchronous call. Instead the call returns a Promise object (synchronously) and you attach the callbacks to that.

UI5 does use promises too (and they are becoming more prevalent).

For a full explanation of promises see MDN or Google.  Some advantages of promises over the old pattern are:

  • they can be chained together
  • the callback is guaranteed to run even if it’s added after the asynchronous call has ended(!)
  • they are easy to read
  • callback functions don’t need to take care of which function gets called next
  • error handling is more sophisticated

Another advantage is the Promise.all method which is so cool and useful it gets it own blog (this one).  This method returns a kind of ‘super-promise’ which will resolve only when each of it’s ‘sub-promises’ resolves (i.e. completes successfully).

In our case we can return three promises, for the sections, questions and answers calls.  Then we write the logic to combine all the results (and build the hierarchy) in the Promise.all success callback.

In the meantime we can sit back and relax, safe in the knowledge that we will be informed when all the callbacks are done!

What does the code look like?

readChecklist: function() {
  var that = this;
			
  Promise.all([ this.readChecklistEntity("/Section"),
		        this.readChecklistEntity("/Question"),
		        this.readChecklistEntity("/Answer")
	      ]).then(that.buildChecklist.bind(that),
                  that.handleChecklistError.bind(that));
},

readChecklistEntity: function(path) {
  var that = this;
			
  return new Promise(
	function(resolve, reject) {
				    that.getModel().read(path, {
					  success: function(oData) {
					    resolve(oData);
					  },
                      error: function(oResult) {
					    reject(oResult);
                      }
	                });
        });
},

buildChecklist: function(values) {
  var aSections  = values[0].results;
  var aQuestions = values[1].results;
  var aAnswers   = values[2].results;
  //...now build the hierarchy
},

handleChecklistError: function(reason) {
  //handle errors			
},

We use a single read function (readChecklistEntity) and call it three times, passing in the entityset name.  That function returns a promise in each case, and we pass the three promises to the Promise.all method.  Then we attach success and error callback functions to the ‘super-promise’ that Promise.all returns.

Notice that this approach allows us to use promises for our calls even though, as noted before, the v2.ODataModel uses the ‘old-style’ callbacks.

Our buildChecklist function will be called when all three of the reads have returned successfully.  We don’t care which comes back first and which comes back last.  The ordering of the result sets matches the order in which we passed the three promises to Promise.all.  It too doesn’t depend on which read is first to complete.

If any of the reads fail, our handleChecklistError function will be called.

Alternatives

As it happens, with default settings our three reads will be combined into one batch request. We could just use the attachBatchRequestCompleted method on v2.ODataModel, which would give us an event handler that is called when the batch request returns.  However, we won’t have handy access to the response data. Also, we might want to combine reads from different OData services and these obviously won’t be combined into a single batch.

Further Reading

Want to learn more about using promises with UI5 and OData? Check out this blog from Wouter Lemaire which outlines a ‘Core Service’ approach built using promises

Nathan Hand has started a blog series on using promises with UI5

Assigned Tags

      13 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Nabheet Madan
      Nabheet Madan

      Wow..this the find of the day Promise.All, for sure we will have N number of use cases. I was trying to read more on to it and came across another interesting one Promise.race method:) It moves forward as soon as one is resolved or rejected. Interesting stuff.

      Thanks for the blog Mike.

      Author's profile photo Mike Doyle
      Mike Doyle
      Blog Post Author

      You're welcome, Nabheet, glad you found it useful.  I believe Promise.race can be used to set a timeout for a particularly expensive asynchronous call.  One of the promises passed is the call itself, the other is a timeout.  If the timeout resolves (or rejects) first then the action is cancelled and the user is informed.

       

      Author's profile photo Volker Buzek
      Volker Buzek

      async code, Promises...yeah!!!

      And: Promise.all() and Promises in general can be safely used with IE in UI5-verse, already since UI5 1.38: https://openui5.hana.ondemand.com/1.38.0/resources/sap/ui/thirdparty/es6-promise-dbg.js

      It's a small Polyfill that bolts Promise onto IE even < 9!

       

      Author's profile photo Sivajan Kumaran
      Sivajan Kumaran

      Nice blog Mike.

      Just to add to Volkers comment; es6-promise polyfill is enforced by sapui5 library even for Microsoft Edge and Safari browsers even though they have their own native Promise implementation. The implementation isn't fully ES6 spec compliant.

      Feel free to use all Promise functions regardless of the browser!

      Author's profile photo Mike Doyle
      Mike Doyle
      Blog Post Author

      Good to know, glad you liked the blog

       

      Author's profile photo Mike Doyle
      Mike Doyle
      Blog Post Author

      Thanks for pointing that out, I've updated the text

       

      Author's profile photo Michelle Crapo
      Michelle Crapo

      And snap - my mind is gone after reading this one.  I'll be coming back to it later - not on a Monday.  I've never worked with promise functions.   That's going to be interesting.  While reading this I was thinking about brfplus.   Of course because I have just used that.  Then I realized this is even more flexible.  Since I learned something my week is done.  OK not really... But it would be nice.

      Thank you for a great read!

      Michelle

      Author's profile photo Mike Doyle
      Mike Doyle
      Blog Post Author

      It took me a while to get the hang of it, too, but it's worth the investment.  It can make your code much simpler.  Thanks for stopping by!

      Author's profile photo Krishna Kishor Kammaje
      Krishna Kishor Kammaje

      Very well written and super useful blog. Thank you.

      Author's profile photo Karthik S
      Karthik S

      Hello Mike,

      Good Post with example. I have a doubt here.What if any one  of my batch call fails when rest all  are in success. Ex: Value1 is failed, value2 and value 3 are in success. Will I get Value2 and Value3 data even though value1 call is fails?

       

      Regards

      Karthik S

      Author's profile photo Mike Doyle
      Mike Doyle
      Blog Post Author

      No, if any of the individual promises fail the aggregated promise will immediately be rejected.  With Promise.all you only get the results if every call was successful.  That's by design

      Author's profile photo Sakthi kumar
      Sakthi kumar

      Mike Doyle

      Hi have two odata calls in two methods.

      function1(){ return new Promise(resolve,reject){odata.read(succes:{resolve(odataResponse)})}}

      function2(){ odata.Create(do something}

       

      from a different function, say function3, the call is like this:

      function3()

      {

      this.Promise =  this.function1();
      this.Promise.then( this.function2(),this );

      }

       

       

      The problem is function1 is executed till odata read call. Then it jumps to function2, executes the create call and then executes the read call in function1.

       

      I want the read call to be executed first and then create call should happen. Can you please help??

      Author's profile photo Mike Doyle
      Mike Doyle
      Blog Post Author

      Hi Sakthi, if I understand you correctly, you want to make a read, then use the results of the read in the call to create a different entity?

      First off I suggest you read the Using Promises page on MDN

      Function 3 should contain logic like this:

      this.function1()
      .then(function(results) {
        this.function2(results);
      }.bind(this);

      Once you have this working you should look into error handling (see catch, finally).  Then you might like to read up on arrow functions.  These allow you to be more concise in your 'then' handler and also make the bind redundant.

      Good luck with promises!