Technical Articles
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
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.
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.
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.jsIt's a small Polyfill that bolts
Promise
onto IE even < 9!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!
Good to know, glad you liked the blog
Thanks for pointing that out, I've updated the text
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
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!
Very well written and super useful blog. Thank you.
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
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
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??
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:
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!