27

I wrote some code that uses promises. It seems that in the Promise environment, I can queue an action, and the action callback is never called. In the callback environment, when I queue an action, the action callback is always called.

I made a test case. If you run it and click Do Operations Using Callbacks, it will run. If you use 1 as the input number, the results will get filled in as 2, 12, and 36.

If you click Do Operations Using Promises, the first result, 2, will be filled in, and then it will stop. If you open the developer tools and go to the console tab, you can see what's going on. I put a bunch of console logs in the program. You can see the action get enqueued. But then the action callback isn't called.

These operations are just times2, add10, and times3. These are just dummy operations. In real life, op3 might be sendDataToSomeoneWhoNeedsIt. Op2 might be collectTogetherTheData. And op2 may not be able to get all of the data itself, so it might call op1, getThePortionOfTheDataThatRequiresUserInput.

Okay, hold your breath! I'm going to type in all the files. (Is there some jsfiddle for Salesforce?)

Apex class cubsSvrThreeOperationsController

public with sharing class cubsSvrThreeOperationsController {

    @AuraEnabled
    // public static Integer times2(Integer num) {return 2 * num;}
    // Nope.  Can't handle Integer yet.
    public static String times2(String numstr) {
        Integer num = Integer.valueOf(numstr);
        num *= 2;
        String numstr2 = String.valueOf(num);
        return numstr2;
    }

    @AuraEnabled
    public static String add10(String numstr) {
        Integer num = Integer.valueOf(numstr);
        num += 10;
        String numstr2 = String.valueof(num);
        return numstr2;
    }

    @AuraEnabled
    public static String times3a(String numstr) {
        Integer num = Integer.valueOf(numstr);
        num *= 3;
        String numstr2 = String.valueof(num);
        return numstr2;
    }
    // I had to name this times3a because times3 got stuck in 
    // the Salesforce internals, and couldn't be recognized.  
    // Thief!  Baggins!  We hates it, we hates it, we hates it 
    // forever!

}

cubsThreeOperationsApp.app

<aura:application >
    <c:cubsThreeOperations />
</aura:application>

cubsThreeOperations.cmp

<aura:component controller="cubsSvrThreeOperationsController" implements="flexipage:availableForAllPageTypes">
    <aura:attribute name="input" type="integer" default="1"/>
    <aura:attribute name="output1" type="integer" default="0"/>
    <aura:attribute name="output2" type="integer" default="0"/>
    <aura:attribute name="output3" type="integer" default="0"/>
    <div>
        <label>Input number: &nbsp;&nbsp;</label>
        <ui:inputNumber value="{!v.input}"/> &nbsp;&nbsp;
        <ui:button label="Do Operations Using Callbacks" press="{!c.doOperationsUsingCallbacks}"/>
                &nbsp;&nbsp;
        <ui:button label="Do Operations Using Promises" press="{!c.doOperationsUsingPromises}"/>
                &nbsp;&nbsp;
        <ui:button label="Do Operations Using getCallback" press="{!c.doOperationsUsingGetCallback}"/>
                &nbsp;&nbsp;
    </div>
    <div>
        <label>Result of operation 1: &nbsp;&nbsp;</label>
        <ui:outputNumber value="{!v.output1}"/>
    </div>
    <div>
        <label>Result of operation 2: &nbsp;&nbsp;</label>
        <ui:outputNumber value="{!v.output2}"/>
    </div>
    <div>
        <label>Result of operation 3: &nbsp;&nbsp;</label>
        <ui:outputNumber value="{!v.output3}"/>
    </div>
    <br/>
</aura:component>

cubsThreeOperationsController.js

({
    doOperationsUsingCallbacks: function (cmp, event, helper) {
        cmp.set('v.output1', 0);
        cmp.set('v.output2', 0);
        cmp.set('v.output3', 0);
        helper.doOperationsUsingCallbacks(cmp, event, helper);
    },

    doOperationsUsingPromises: function (cmp, event, helper) {
        cmp.set('v.output1', 0);
        cmp.set('v.output2', 0);
        cmp.set('v.output3', 0);
        helper.doOperationsUsingPromises(cmp, event, helper);
    },

    doOperationsUsingGetCallback: function (cmp, event, helper) {
        cmp.set('v.output1', 0);
        cmp.set('v.output2', 0);
        cmp.set('v.output3', 0);
        helper.doOperationsUsingGetCallback(cmp, event, helper);
    }
})

cubsThreeOperationsHelper.js

({
    doOperationsUsingCallbacks: function (cmp, event, helper) {
        var num = cmp.get('v.input');
        helper.doOperationCallback3(cmp, event, helper, num);
    },

    // Operation 3 defines its result in terms of values it doesn't have yet.
    doOperationCallback3: function (cmp, event, helper, num) {
        helper.doOperationCallback2(cmp, event, helper, num, function (cmp, event, helper, num) {
            var action = cmp.get('c.times3a');
            var numstr = num.toString();
            action.setParams({
                numstr: numstr
            });
            action.setCallback(this, function (response) {
                var state = response.getState();
                if (state === 'SUCCESS') {
                    var outNumStr = response.getReturnValue();
                    var outNum = parseInt(outNumStr);
                    cmp.set('v.output3', outNum);
                } else {
                    console.log('Failed');
                }
            });
            $A.enqueueAction(action);
        });
    },

    doOperationCallback2: function (cmp, event, helper, num, callback) {
        helper.doOperationCallback1(cmp, event, helper, num, function (cmp, event, helper, num) {
            var action = cmp.get('c.add10');
            var numstr = num.toString();
            action.setParams({
                numstr: numstr
            });
            action.setCallback(this, function (response) {
                var state = response.getState();
                if (state === 'SUCCESS') {
                    var outNumStr = response.getReturnValue();
                    var outNum = parseInt(outNumStr);
                    cmp.set('v.output2', outNum);
                    callback(cmp, event, helper, outNum);
                } else {
                    console.log('Failed');
                }
            });
            $A.enqueueAction(action);
        });
    },

    doOperationCallback1: function (cmp, event, helper, num, callback) {
        var action = cmp.get('c.times2');
        var numstr = num.toString();
        action.setParams({
            numstr: numstr
        });
        action.setCallback(this, function (response) {
            var state = response.getState();
            if (state === 'SUCCESS') {
                var outNumStr = response.getReturnValue();
                var outNum = parseInt(outNumStr);
                cmp.set('v.output1', outNum);
                callback(cmp, event, helper, outNum);
            } else {
                console.log('Failed');
            }
        });
        $A.enqueueAction(action);
    },

    doOperationsUsingPromises: function (cmp, event, helper) {
        var num = cmp.get('v.input');
        helper.doOperationPromise3(cmp, event, helper, num);
    }, 

    doOperationPromise3: function (cmp, event, helper, num) {
        console.log('doOperationPromise3 - entered');
        helper.doOperationPromise2(cmp, event, helper, num)
        .then(function (num) {
            console.log('doOperationPromise3 - entered then function');
            var action = cmp.get('c.times3a');
            var numstr = num.toString();
            action.setParams({
                numstr: numstr
            });
            action.setCallback(this, function (response) {
                console.log('doOperationPromise3 - entered action callback');
                var state = response.getState();
                if (state === 'SUCCESS') {
                    var outNumStr = response.getReturnValue();
                    var outNum = parseInt(outNumStr);
                    cmp.set('v.output3', outNum);
                } else {
                    console.log('Failed');
                }
            });
            console.log('doOperationPromise3 - about to enqueue action');
            $A.enqueueAction(action);
        }, function () {});
    }, 

    doOperationPromise2: function (cmp, event, helper, num) {
        console.log('doOperationPromise2 - entered');
        return new Promise(function (resolve, reject) {
            console.log('doOperationPromise2 - entered promise function');
            helper.doOperationPromise1(cmp, event, helper, num)
            .then(function (num) {
                console.log('doOperationPromise2 - entered then function');
                var action = cmp.get('c.add10');
                var numstr = num.toString();
                action.setParams({
                    numstr: numstr
                });
                action.setCallback(this, function (response) {
                    console.log('doOperationPromise2 - entered action callback');
                    var state = response.getState();
                    if (state === 'SUCCESS') {
                        var outNumStr = response.getReturnValue();
                        var outNum = parseInt(outNumStr);
                        cmp.set('v.output2', outNum);
                        console.log('doOperationPromise2 - about to resolve promise');
                        resolve(outNum);
                    } else {
                        console.log('Failed');
                        reject();
                    }
                });
                console.log('doOperationPromise2 - about to enqueue action');
                $A.enqueueAction(action);
            }, function () {});
        });
    },

    doOperationPromise1: function (cmp, event, helper, num) {
        console.log('doOperationPromise1 - entered');
        return new Promise(function (resolve, reject) {
            console.log('doOperationPromise1 - entered promise function');
            var action = cmp.get('c.times2');
            var numstr = num.toString();
            action.setParams({
                numstr: numstr
            });
            action.setCallback(this, function (response) {
                console.log('doOperationPromise1 - entered action callback');
                var state = response.getState();
                if (state === 'SUCCESS') {
                    var outNumStr = response.getReturnValue();
                    var outNum = parseInt(outNumStr);
                    cmp.set('v.output1', outNum);
                    console.log('doOperationPromise1 - about to resolve promise');
                    resolve(outNum);
                } else {
                    console.log('Failed');
                    reject();
                }
            });
            console.log('doOperationPromise1 - about to enqueue action');
            $A.enqueueAction(action);
        });
    },


    doOperationsUsingGetCallback: function (cmp, event, helper) {
        var num = cmp.get('v.input');
        helper.doOperationGetCallback3(cmp, event, helper, num);
    }, 

    doOperationGetCallback3: function (cmp, event, helper, num) {
        console.log('doOperationGetCallback3 - entered');
        helper.doOperationGetCallback2(cmp, event, helper, num)
        .then($A.getCallback(function (num) {
            console.log('doOperationGetCallback3 - entered then function');
            var action = cmp.get('c.times3a');
            var numstr = num.toString();
            action.setParams({
                numstr: numstr
            });
            action.setCallback(this, function (response) {
                console.log('doOperationGetCallback3 - entered action callback');
                var state = response.getState();
                if (state === 'SUCCESS') {
                    var outNumStr = response.getReturnValue();
                    var outNum = parseInt(outNumStr);
                    cmp.set('v.output3', outNum);
                } else {
                    console.log('Failed');
                }
            });
            console.log('doOperationGetCallback3 - about to enqueue action');
            $A.enqueueAction(action);
        }), $A.getCallback(function () {}) );
    }, 

    doOperationGetCallback2: function (cmp, event, helper, num) {
        console.log('doOperationGetCallback2 - entered');
        return new Promise(function (resolve, reject) {
            console.log('doOperationGetCallback2 - entered promise function');
            helper.doOperationPromise1(cmp, event, helper, num)
            .then($A.getCallback(function (num) {
                console.log('doOperationGetCallback2 - entered then function');
                var action = cmp.get('c.add10');
                var numstr = num.toString();
                action.setParams({
                    numstr: numstr
                });
                action.setCallback(this, function (response) {
                    console.log('doOperationGetCallback2 - entered action callback');
                    var state = response.getState();
                    if (state === 'SUCCESS') {
                        var outNumStr = response.getReturnValue();
                        var outNum = parseInt(outNumStr);
                        cmp.set('v.output2', outNum);
                        console.log('doOperationGetCallback2 - about to resolve promise');
                        resolve(outNum);
                    } else {
                        console.log('Failed');
                        reject();
                    }
                });
                console.log('doOperationGetCallback2 - about to enqueue action');
                $A.enqueueAction(action);
            }), $A.getCallback(function () {}) );
        });
    },

    doOperationGetCallback1: function (cmp, event, helper, num) {
        console.log('doOperationGetCallback1 - entered');
        return new Promise(function (resolve, reject) {
            console.log('doOperationGetCallback1 - entered promise function');
            var action = cmp.get('c.times2');
            var numstr = num.toString();
            action.setParams({
                numstr: numstr
            });
            action.setCallback(this, function (response) {
                console.log('doOperationGetCallback1 - entered action callback');
                var state = response.getState();
                if (state === 'SUCCESS') {
                    var outNumStr = response.getReturnValue();
                    var outNum = parseInt(outNumStr);
                    cmp.set('v.output1', outNum);
                    console.log('doOperationGetCallback1 - about to resolve promise');
                    resolve(outNum);
                } else {
                    console.log('Failed');
                    reject();
                }
            });
            console.log('doOperationGetCallback1 - about to enqueue action');
            $A.enqueueAction(action);
        });
    }

})


})

Perhaps promises are just new to Salesforce, and the situation will improve in the coming days. Reference this article: ES6 Promises do not exist in controllers, helpers under Lightning Locker

6
  • 7
    I'm rather annoyed that I can only +1 this once. This is a beautifully written question. Commented Aug 12, 2016 at 14:55
  • 1
    Well then hopefully this OP creates a username and sticks around a while! Commented Aug 13, 2016 at 12:57
  • @DougChasman Please take a look at this post. Commented Aug 13, 2016 at 20:06
  • I opened a case with Salesforce on this 8/7. Case No. 14365254. Still waiting on a resolution. Commented Sep 8, 2016 at 13:54
  • Salesforce gave me this explanation, and then closed the case: "I have got the confirmation that the Promise Class are added to the Locker Service and it is supported by Lightning Components. However, as the Lightning expert confirmed that the issue is not related with the Locker Service." I don't really understand what they mean. They seem to be saying "Promises work with Locker, but this has nothing to do with Locker." Does anybody "get" what they're saying? Commented Sep 16, 2016 at 13:33

2 Answers 2

13

Promises execute in a microtask which, by definition, breaks out of the Lightning event loop and the current Lightning access context. If you're unfamiliar with microtasks please read https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/.

If you need to preserve access context or need to reenter the Lightning event loop use $A.getCallback(). Eg

getAPromise().then(
  $A.getCallback(function resolve(value) { /* resolve handler */ }),
  $A.getCallback(function reject(error) { /* reject handler */ })
)

You need to be within the Lightning event loop when you enqueue an action (and you need to be within an access context to create the action in the first place). Eg

getAPromise().then(
  $A.getCallback(function resolve(value) { 
    var action = cmp.get("c.method");
    action.setParams({...});
    action.setCallback(scopeVar, function(response) {
      /* check response.getState() and handle accordingly */
    });
    $A.enqueueAction(action);
  }),
  $A.getCallback(function reject(error) { /* reject handler */ })
)

You can learn more about $A.getCallback() at https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/js_cb_mod_ext_js.htm.

Some best practices to follow:

  1. Always include a reject handler or catch in your promise chain. If you need to report an error use $A.reportError(). Throwing an error in a promise will not trigger window.onerror, which is where Lightning hooks up the global error handler. Watch the JS console for reports about uncaught errors in a promise to verify you've done it correctly.

  2. Storable actions (https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/controllers_server_storable_actions.htm) may have their callbacks invoked more than once. This models the inherent mutable nature of Salesforce metadata and data. It's more like streams / reactive programming than most realize. This doesn't align well with the promise flow of 1-time resolve/reject state transition and thus callbacks are used in action.setCallback(scope, callbackFunction).

2
  • Hi, Kevin. Thanks very much for feeding back on this. I am in the process of digesting / trying out what you said. For now: 1) Thanks for the $A.reportError info. 2) Thanks for the "calling a callback multiple times" info. I'm glad for the chance to get the architecture in my head. Commented Sep 29, 2016 at 16:07
  • I think I get it now, Kevin. My coding example had a callbacks feature and a promises feature. Now it has a getCallbacks feature. I made changes in three files: 1) cubsThreeOperations.cmp I added a Do Operations Using getCallback button. 2) cubsThreeOperationsController.js I added a doOperationsUsingGetCallback function. 3) cubsThreeOperationsHelper.js I added a doOperationsUsingGetCallback function, and its three helper functions. Maybe the next guy will benefit by this example. Commented Sep 29, 2016 at 20:36
-3

In lightning all the action calls are async and will sun in batches. If in a case your component starts rendering before the call returns successfully you may try using aura:doneWaiting.

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.