JavaScript Promises in Lightning Components
Introduction
Promises have been around for a few years now, originally in libraries or polyfills but now natively in JavaScript for most modern browsers (excluding IE11, as usual!).
The Mozilla Developer Network provides a succinct definition of JavaScript promises:
A
Promise
is a proxy for a value not necessarily known when the promise is created
Promises can be in one of three states:
- pending - not fulfilled or rejected (Heisenberg, for Breaking Bad fans!)
- fulfilled - the asynchronous code successfully completed
- rejected - an error occurred executing the asynchronous code
For me, the key advantage with Promises is that they allow asynchronous JavaScript code to be written in a way that looks somewhat like synchronous code, and is thus easier for someone new to the implementation to understand.
Creating a Promise
var promise = new Promise(function(resolve, reject) { // asynchronous code goes here if (success) { /* everything worked as expected */ resolve("Excellent :)"); } else { /* something went wrong */ reject(Error("Bogus :(")); } });
The Promise constructor takes a callback function as a parameter. This callback function contains the asynchronous code to be executed (to retrieve a record from the Salesforce server, for example). The callback function in turn takes two parameters that are also functions - note that you don’t have to write these functions, just invoke them based on the outcome of the asynchronous code:
- resolve - this function is invoked if the asynchronous code executes successfully. Executing this function moves the Promise to the fulfilled state
- reject - this function is invoked with an Error if anything goes wrong in the asynchronous code. Executing this function moves the Promise to the rejected state.
Handling the Result
Thus far all well and good, but pretty much all of the asynchronous code that I write is carrying out a remote activity and returning the result, so I need some way to be notified when the asynchronous code has completed. Enter the Promise.then() function:
promise.then(function(data) { alert('Success : ' + data); }, function(error) { alert('Failure : ' + error.message); });
Promise.then() takes two functions as parameters - the first is a success callback, invoked if and when the promise is resolved, and the second is an error callback, invoked if and when the promise is rejected.
A Real World Example
When building Lightning Components, asynchronous interaction with the Salesforce server is typically carried out via an action. The following function takes an action and creates a Promise around it:
executeAction: function(cmp, action, callback) { return new Promise(function(resolve, reject) { action.setCallback(this, function(response) { var state = response.getState(); if (state === "SUCCESS") { var retVal=response.getReturnValue(); resolve(retVal); } else if (state === "ERROR") { var errors = response.getError(); if (errors) { if (errors[0] && errors[0].message) { reject(Error("Error message: " + errors[0].message)); } } else { reject(Error("Unknown error")); } } }); $A.enqueueAction(action); }); }
the executeAction function instantiates a Promise that defines the action callback handler and enqueues the action. When the action completes, the callback handler determines whether to fulfil or reject the Promise based on the state of the response.
This function can then be used to create a Promise to retrieve an account:
var accAction = cmp.get("c.GetAccount"); var params={"accountIdStr":accId}; accAction.setParams(params); var accountPromise = this.executeAction(cmp, accAction);
and callback handlers provided to process the results:
accountPromise.then( $A.getCallback(function(result){ // We have the account - set the attribute cmp.set('v.account', result); }), $A.getCallback(function(error){ // Something went wrong alert('An error occurred getting the account : ' + error.message); }) );
Note that the success and error callbacks are encapsulated in $A.getCallback functions as they are executed asynchronously, and therefore are outside of the Lightning Components lifecycle. Note also that if you forget to do this, quite a lot of the promise functionality will still work, which will make it difficult to track down what the exact problem is!
Chaining Promises
The Promise.then() function can return another Promise, thus setting up a chain of asynchronous operations that each complete in turn before the next one can start. Repurposing the above example to retrieve a Contact from the Account:
accountPromise.then( $A.getCallback(function(result){ // We have the account - set the attribute cmp.set('v.account', result); // return a promise to retrieve a contact var contAction = cmp.get("c.GetContact"); var contParams={"accountIdStr":accId}; contAction.setParams(contParams); var contPromise=self.executeAction(cmp, contAction); return contPromise; }), $A.getCallback(function(error){ // Something went wrong alert('An error occurred getting the account : ' + error.message); }) ) .then( $A.getCallback(function(result){ // We have the contact - set the attribute cmp.set('v.contact', result); }), $A.getCallback(function(error){ // Something went wrong alert('An error occurred getting the contact : ' + error.message); }) );
However there is a side effect here - the second then() is executed regardless of the success/failure of the first. If the first Promise was rejected, the success callback for the second then() is executed with a null value. While this would be benign in the above example, it’s probably not behaviour that is desired in most cases. What would be better is that the second then() is only executed if the first one is successful. Enter the Promise.catch() function.
Catching Errors
The Promise.catch() function is invoked if a Promise is rejected, but the then() function didn’t provide an error callback:
promise.then(function(data) { alert('Success : ' + data); }) .catch(function(error) { alert('Failure : ' + error.message); });
When chaining Promises, the catch() function becomes more powerful, as if a Promise is rejected and the then() function did not provide an error callback, control moves forward to either the next then() function that does provide an error callback, or the next catch() function.
Refactoring the example again:
accountPromise.then( $A.getCallback(function(result){ // We have the account - set the attribute cmp.set('v.account', result); // return a promise to retrieve a contact var contAction = cmp.get("c.GetContact"); var contParams={"accountIdStr":accId}; contAction.setParams(contParams); var contPromise=self.executeAction(cmp, contAction); return contPromise; }) ) .then( $A.getCallback(function(result){ // We have the contact - set the attribute cmp.set('v.contact', result); }) .catch( $A.getCallback(function(error){ // Something went wrong alert('An error occurred : ' + e.message); }) );
The second then() function is now only executed if the account retrieval is successful. An error occurring retrieving either the account or the contact immediately jumps forward to the catch() function to surface the error.
Going back to the original point made in the introduction, I now have two asynchronous operations, with the second dependent on the success of the first, but coded in a readable fashion.
Further Reading
Promises are a tricky one to wrap your head around, and its certainly worth spending some time learning the basics and playing around with examples. I've found the following resources very useful:
- JavaScript Promises: an Introduction
- An Overview of JavaScript Promises
- Understanding JavaScript Promises
Related Posts
- LDS Activity Timeline, Lightning Components and Visualforce
- Lightning Components and JavaScript Libraries
- Lightning Component Events
- Lightning Components and Unobtrusive JavaScript
- Lightning Components and Custom Apex Classes
Thanks Keir, great Post.
ReplyDeleteLove it, Thanks!!
ReplyDeleteI'd like to use this, but when it gets to self.executeAction (i.e. the second action), I get a runtime error:
ReplyDeleteError in $A.getCallback() [self.executeAction is not a function]
Does this still work in Spring '18 or have I done something wrong?
Mike
The second call to executeAction does not work for me. I get the error:
ReplyDeleteError in $A.getCallback() [self.executeAction is not a function]
Any suggestions?
Mike
This is very useful, thank you so much!!!
ReplyDeleteThis comment has been removed by a blog administrator.
ReplyDeleteWhat are the implications of wrapping the $A.getCallback into a promise? Will this work or will we still have the same issues as if we didn't ue $A.getCallback?
ReplyDeleteHi Bob, I am having an issue where using promises is causing the action to be ABORTED. The server side action is executed but the callback is never executed. Any experience with this? Need help.
ReplyDelete