30

How can I perform dynamic chaining in Javascript Promises, all the time I have seen only hardcoding of the calls for eg., (promise).then(request/functionName).then(request/functionName)

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
R Ajay
  • 311
  • 1
  • 3
  • 9

10 Answers10

52

Given an array functions that all return promises, you can use reduce() to run them sequentially:

var myAsyncFuncs = [
    function (val) {return Promise.resolve(val + 1);},
    function (val) {return Promise.resolve(val + 2);},
    function (val) {return Promise.resolve(val + 3);},
];

myAsyncFuncs.reduce(function (prev, curr) {
    return prev.then(curr);
}, Promise.resolve(1))
.then(function (result) {
    console.log('RESULT is ' + result);  // prints "RESULT is 7"
});

The example above uses ES6 Promises but all promise libraries have similar features.

Also, creating the array of promise returning functions is usually a good candidate for using map(). For example:

myNewOrmModels.map(function (model) {
    return model.save.bind(model);
}).reduce(function (prev, curr) {
    return prev.then(curr);
}, Promise.resolve())
.then(function (result) {
    console.log('DONE saving');
});
wuher
  • 1,629
  • 15
  • 12
  • this is the neatest answer – user2954463 Mar 15 '17 at 14:29
  • Any thoughts on how to avoid the memory wastage caused by `reduce`? – Salman Dec 05 '17 at 15:35
  • @Салман You can do the same thing reduce does in a `for` loop. Create a variable containing a promise (e.g. `var myPromise = Promise.resolve()`) then loop through your array of functions and **re-set your promise variable** with the each step (e.g. `myPromise = myPromise.then(...)`). If you don't reset your promise variable, all your async functions will be run in parallel, not in step. – MynockSpit Mar 22 '18 at 05:13
  • Sorry I wasn't clear then. I meant memory wastage due to creation of so many promises. For example I have 100 things to do in sequence and I do reduce/for and chain 100 promises. I create 100 promises and consume all that memory. If say my 2nd promise rejects, none further will be useful and the control goes to catch directly. That's what I mean when I say optimising memory usage. I'm wondering if we can only create promise what we want to execute next. – Salman Mar 22 '18 at 06:08
  • Didn't test it yet, but that looks neat af! – Milkncookiez Apr 20 '18 at 22:07
  • If I use code like this, I get eslint warnings "*Avoid nesting promises promise/no-nesting*". Is this inevitable? – Duncan Jones Jul 24 '18 at 18:12
  • For the sake of showing they run sequentially, the above might be changed to have different mathematical operations other than +. – Tyler Chong Jun 20 '19 at 21:25
9

One option is to utilize the properties of objects and the ability to invoke them via strings.

I wrote a small sample Here and posted it below.

The idea is that you have the set of functions that you wish to run set in some namespace or object, as I did in 'myNamespace':

myNamespace = {
    "A": function() {return "A Function";},
    "B": function() {return "B Function";},
    "C": function() {return "C Function";}
}

Then your main promise would run and somehow (via inputs, ajax, prompts, etc.) you would get the string value of the function you want to have run, which isn't known until runtime:

My main promise uses a prompt to get a letter from the user:

var answer = prompt('Starting.  Please pick a letter: A,B,C');
        if(myNamespace[answer] === undefined)
        {
            alert("Invalid choice!");
            reject("Invalid choice of: " + answer);
        }
        else
        {
            resolve(answer);
        }

In the next 'then' I use that value (passed via the resolve function) to invoke the function:

.then(function(response) {
        funcToRun = myNamespace[response]();})

Finally, I output to html the result of my dynamic function call and I use some recursive fun to make it more interactive and demonstrate that it is dynamic:

.then(function(){
        document.getElementById('result').innerHTML = funcToRun;})
    .then(function(){
        if(prompt("Run Again? (YES/NO)")==="YES")
        {
            doWork();
        }
    });

myNamespace = {
    "A": function() {return "A Function";},
    "B": function() {return "B Function";},
    "C": function() {return "C Function";}
}

function doWork()
{
    var funcToRun;
    
    new Promise(function(resolve,reject) {
        var answer = prompt('Starting.  Please pick a letter: A,B,C');
        if(myNamespace[answer] === undefined)
        {
            alert("Invalid choice!");
            reject("Invalid choice of: " + answer);
        }
        else
        {
            resolve(answer);
        }
    })
    .then(function(response) {
        funcToRun = myNamespace[response]();})
    .then(function(){
        document.getElementById('result').innerHTML = funcToRun;})
    .then(function(){
        if(prompt("Run Again? (YES/NO)")==="YES")
        {
            doWork();
        }
    });
}

doWork();
<div id="result"></div>
JasonWilczak
  • 2,303
  • 2
  • 21
  • 37
  • Thank You Jason... I will try implementing the same and let you know – R Ajay Jun 15 '15 at 20:51
  • Hi Jason....Quick Question....I will get many requests(fns to be called) from the user actions, How can I check the current promise status and then chain if it is still pending or just run directly if the status is resolved – R Ajay Jun 29 '15 at 17:55
  • @RamuAjay so, there is a Promise state value, but it is private. My guess is to use a `.done(function() {isPromiseDone = true;})` or some external variable to hold the current promise state. As for adding additional "thens" you could hold an array of functions and in the `.done()` function you could check that array and append more "thens" to the promise... that is just a guess though. – JasonWilczak Jun 30 '15 at 12:38
6

Since promises unwrap, just continue to add then statements and it will continue to be chained together

function asyncSeries(fns) {
  return fns.reduce(function(p, fn) {
    return p.then(fn);
  }, Promise.resolve());
}

Recursively is a pretty cool way to do it as well :)

function countTo(n, sleepTime) {
  return _count(1);

  function _count(current) {
    if (current > n) {
      return Promise.resolve();
    }

    return new Promise(function(resolve, reject) {
      console.info(current);
      setTimeout(function() {
        resolve(_count(current + 1));
      }, sleepTime);
    });
  }
}
Travis Kaufman
  • 2,867
  • 22
  • 23
  • Hi Travis....Quick Question....I will get many requests(fns to be called) from the user actions, How can I check the current promise status and then chain to it if it is still pending or just run next function directly if the promise state is settled (resolved or rejected) – – R Ajay Jun 30 '15 at 11:43
  • You wrapping the recursive call to the function in a resolve() is what finally got the code I'd been struggling with to work. Thanks so much for the recursive version. – kevinmicke Jan 27 '19 at 03:48
6

This is ES7 way.

Let's say you have multiple promises defined in an array.

  var funcs = [
    _ => new Promise(res => setTimeout(_ => res("1"), 1000)),
    _ => new Promise(res => setTimeout(_ => res("2"), 1000))
  }

And you want to call like this.

 chainPromises(funcs).then(result => console.log(result));

You can use async and await for this purpose.

  async function chainPromises(promises) {
    for (let promise of promises) {  // must be for (.. of ..)
      await promise();
    }
  }

This will execute the given functions sequentially(one by one), not in parallel. The parameter promises is an array of functions, which return Promise.

Plunker: http://plnkr.co/edit/UP0rhD?p=preview

allenhwkim
  • 27,270
  • 18
  • 89
  • 122
2

I think the simplest way is:

const executePromises = function(listOfProviders){

    const p = Promise.resolve(null);

    for(let i = 0; i < listOfProviders.length; i++){
       p = p.then(v => listOfProviders[i]());
    }

   return p;

};

I believe the above is basically equivalent to:

const executePromises = async function(listOfProviders) {

    for(let i = 0; i < listOfProviders.length; i++){
       await listOfProviders[i]();
    }

};
Alexander Mills
  • 90,741
  • 139
  • 482
  • 817
1

This solution based on usage promises of introduced in the EcmaScript 6 (https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise), so before use it see table browser`s support https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise#Browser_compatibility

Code

var f1 = function(){
    for (var i = 0; i < 800000000; i++) {}
    console.log('Function1 is done');
}
var f2 = function(){
    for (var i = 0; i < 800000000; i++) {}
    console.log('Function2 is done');
}
var f3 = function(){
    for (var i = 0; i < 800000000; i++) {}
    console.log('Function3 is done');
}
var f4 = function(){
    for (var i = 0; i < 800000000; i++) {}
    console.log('Function4 is done');
}


callbacks = function(){

    // copy passed arguments
    var callbacks = arguments;

    // create array functions
    var callbacks = Object.keys(callbacks).map(function(el){ return callbacks[el] });

    var now = Date.now();

    callbacks.reduce(function(previousPromise, currentFunc){
        return previousPromise.then(
            function(){
                currentFunc();
                var seconds = (Date.now() - now) / 1000;
                console.log('Gone', seconds, 'seconds');
            }
        )
    }, Promise.resolve());
}

callbacks(f1, f2, f3, f4);

Result in Chrome console (values seconds will be different):

Function1 is done
Gone 1.147 seconds
Function2 is done
Gone 2.249 seconds
Function3 is done
Gone 3.35 seconds
Function4 is done
Gone 4.47 seconds

Notes:

  1. It is does not work if a function contains a timer (for this problem I try also jQuery`s $Callbacks, $.Ajax and $.When but it not help. The only decision, what I found, usage resolve() in callback of a timer, but it is not acceptable if you have completed functions.).
  2. Testing environment

$ google-chrome --version
Google Chrome 53.0.2785.116
PADYMKO
  • 4,217
  • 2
  • 36
  • 41
1

I just had a problem with my api provider that doing Promise.all() would end up in concurrency db problems..

The deal with my situation is that i need to get every promise result in order to show some "all ok" or "some got error" alert.

And i don't know why .. this little piece of code who uses reduce when the promises resolved i couldn't get my scope to work (too late to investigate now)

$scope.processArray = function(array) {
    var results = [];
    return array.reduce(function(p, i) {
        return p.then(function() {
            return i.then(function(data) {
                results.push(data);
                return results;
            })
        });
    }, Promise.resolve());
}

So thanks to this post http://hellote.com/dynamic-promise-chains/ I came with this little bastard.. It's not polished but it's working all right.

$scope.recurse = function(promises, promisesLength, results) {

    if (promisesLength === 1) {
        return promises[0].then(function(data){
            results.push(data);
            return results;
        });
    }

    return promises[promisesLength-1].then(function(data) {
        results.push(data);
        return $scope.recurse(promises, promisesLength - 1, results);
    });

}

Then i invoke the function like this:

var recurseFunction = $scope.recurse(promises, promises.length, results);
recurseFunction.then(function (response) { ... });

I hope it helps.

Daniel S.
  • 730
  • 6
  • 14
0

To build on this answer, if you want to return the result of all those promises, too:

async function chainPromises(promises) {
    let results = [];
    for (let promise of promises) { 
        let result = await promise();
        results.push(result);
    }
    return results;
}
Brad
  • 11,934
  • 4
  • 45
  • 73
0

Actually then provides dynamic chaining because it immediately returns an equivalent Promise object, allowing you to chain calls to other promise methods.

Let take a look on the the following code:

const promise = new Promise(resolve => setTimeout(() => resolve(1), 1000));

promise
  .then(value => value + 10)
  .then(value => value + 100)
  .then(value => console.log(value)) // Prints: 111

We can rewrite this in a way:

const promise2 = promise.then(value => value + 10);
const promise3 = promise2.then(value => value + 100);
const promise4 = promise3.then(value => console.log(value)); // Prints: 111

As you can see from above we are able to append next then separately one by one.

This is the generic approach to manage the promise chains by keeping the latest Promise object:

class PromiseChain {
  constructor(promise) {
    this.promise = promise;
  }

  then(onFulfilled, onRejected) {
    this.promise = this.promise.then(onFulfilled, onRejected);
  }
}

const promiseChain = new PromiseChain(promise);
promiseChain.then(value => value + 10);
promiseChain.then(value => value + 100);
promiseChain.then(value => console.log(value)); // Prints: 111

It allows us to make the promise chains using conditions, loops etc., for instance:

const promiseChain2 = new PromiseChain(promise);
[
  value => value + 10,
  value => value + 100,
  value => console.log(value),
]
.forEach(item => promiseChain2.then(item)); // Prints: 111
iUrii
  • 11,742
  • 1
  • 33
  • 48
-2

Check the following tutorial for

  1. programmatic (dynamic) chaining of javascript/node.js promises and
  2. Promise chaining using recursive functions

Programmatic-Chaining-and-Recursive-Functions-with-JavaScript-Promise

Tuhin Paul
  • 558
  • 4
  • 11