Dynamic Promise Chains

Click here for a CodePen example - Skip to best answer

Promises can be daunting to newcomers and experts alike. They take everything we've learned about callbacks and turn them around.

Something that is important to understand about promises is that you can do a lot more than just write static promise chains. Typically you might see something like this:

getUserData  
  .then(validateUserData)
  .then(updateUserData)
  .then(attachUserMetadata)
  .then(respond);

That is a great use of promises to keep a program's flow in shape and keep things flowing as you would expect, but not all use cases follow this pattern. What if you don't know what operations need to happen? What if you need to do something an unknown number of times?

Here is the thing: A promise function, and its .then statements all return promises. Duh, right? But the thing to note is that they return the most recent promise. That is how chaining works here. .then returns a promise which resolves after the previous promise. This means we can leverage .then to generate a dynamic promise chain.

Let's say we have some reason why we need to execute pong() on odd numbers and ping() on even numbers, counting down from x. Let's also say that both of these functions are promise functions. We can recursively loop to generate a promise chain that is x long, calling the functions we want along the way.

// Recurse on a given promise chain
function recurse(promise, amount) {

  // Base case - just return the promise
  if (amount === 0) {
    return promise;
  }

  var next;

  if (amount % 2 === 0) {
    next = promise.then(ping);
  } else {
    next = promise.then(pong);
  }

  return next.then(function() {
    recurse(next, amount - 1);
  }
}

This is going to do it for us. We pass a promise into our recursion function, and it handles the rest, returning the place for us to chain more .thens on at the end.

We can do better, though. This is a helper function that takes a promise. That isn't something that is super developer friendly. Let's tweak it to encapsulate all of these promises while sticking to a promise chain syntax:

// Recurse on a given amount
function recurse(amount) {

  // Base case - just return the promise
  if (amount === 0) {
    return promise;
  }

  var next;

  if (amount % 2 === 0) {
    next = ping;
  } else {
    next = pong;
  }

  return next.then(function() {
    recurse(amount - 1);
  }
}

Now, we can call

myPromiseFunctionThatResolvesToANumber.then(recurse);  

and it will all be taken care of from there.

These are just two ways of doing this, but you can get creative with loops and other methods of dynamic generation. One gotcha with loops is that you need to be smart with scope. If you are running promises inside of a loop, your promises will be using variable values from within that scope as they stand at the end of the loop. It is recommended that you generate promises in helper functions to better encapsulate scope. Arrow functions may help, but they are not sufficient for fixing the loop-scope scenario.