Why Using reduce() to Sequentially Resolve Promises Works

Writing asynchronous JavaScript without using the Promise object is a lot like baking a cake with your eyes closed. It can be done, but it’s gonna be messy and you’ll probably end up burning yourself.

I won’t say it’s necessary, but you get the idea. It’s real nice. Sometimes, though, it needs a little help to solve some unique challenges, like when you’re trying to sequentially resolve a bunch of promises in order, one after the other. A trick like this is handy, for example, when you’re doing some sort of batch processing via AJAX. You want the server to process a bunch of things, but not all at once, so you space the processing out over time.

Ruling out packages that help make this task easier (like Caolan McMahon’s async library), the most commonly suggested solution for sequentially resolving promises is to use Array.prototype.reduce(). You might’ve heard of this one. Take a collection of things, and reduce them to a single value, like this:

let result = [1,2,5].reduce((accumulator, item) => { return accumulator + item;
}, 0); // <-- Our initial value. console.log(result); // 8

But, when using reduce() for our purposes, the setup looks more like this:

let userIDs = [1,2,3]; userIDs.reduce( (previousPromise, nextID) => { return previousPromise.then(() => { return methodThatReturnsAPromise(nextID); });
}, Promise.resolve());

Or, in a more modern format:

let userIDs = [1,2,3]; userIDs.reduce( async (previousPromise, nextID) => { await previousPromise; return methodThatReturnsAPromise(nextID);
}, Promise.resolve());

This is neat! But for the longest time, I just swallowed this solution and copied that chunk of code into my application because it “worked.” This post is me taking a stab at understanding two things:

  1. Why does this approach even work?
  2. Why can’t we use other Array methods to do the same thing?

Why does this even work?

Remember, the main purpose of reduce() is to “reduce” a bunch of things into one thing, and it does that by storing up the result in the accumulator as the loop runs. But that accumulator doesn’t have to be numeric. The loop can return whatever it wants (like a promise), and recycle that value through the callback every iteration. Notably, no matter what the accumulator value is, the loop itself never changes its behavior — including its pace of execution. It just keeps rolling through the collection as fast as the thread allows.

This is huge to understand because it probably goes against what you think is happening during this loop (at least, it did for me). When we use it to sequentially resolve promises, the reduce() loop isn’t actually slowing down at all. It’s completely synchronous, doing its normal thing as fast as it can, just like always.

Look at the following snippet and notice how the progress of the loop isn’t hindered at all by the promises returned in the callback.

function methodThatReturnsAPromise(nextID) { return new Promise((resolve, reject) => { setTimeout(() => { console.log(`Resolve! ${dayjs().format('hh:mm:ss')}`); resolve(); }, 1000); });
} [1,2,3].reduce( (accumulatorPromise, nextID) => { console.log(`Loop! ${dayjs().format('hh:mm:ss')}`); return accumulatorPromise.then(() => { return methodThatReturnsAPromise(nextID); });
}, Promise.resolve());

In our console:

"Loop! 11:28:06" "Loop! 11:28:06" "Loop! 11:28:06" "Resolve! 11:28:07" "Resolve! 11:28:08" "Resolve! 11:28:09"

The promises resolve in order as we expect, but the loop itself is quick, steady, and synchronous. After looking at the MDN polyfill for reduce(), this makes sense. There’s nothing asynchronous about a while() loop triggering the callback() over and over again, which is what’s happening under the hood:

while (k < len) { if (k in o) { value = callback(value, o[k], k, o); } k++;
}

With all that in mind, the real magic occurs in this piece right here:

return previousPromise.then(() => { return methodThatReturnsAPromise(nextID)
});

Each time our callback fires, we return a promise that resolves to another promise. And while reduce() doesn’t wait for any resolution to take place, the advantage it does provide is the ability to pass something back into the same callback after each run, a feature unique to reduce(). As a result, we’re able build a chain of promises that resolve into more promises, making everything nice and sequential:

new Promise( (resolve, reject) => { // Promise #1 resolve();
}).then( (result) => { // Promise #2 return result;
}).then( (result) => { // Promise #3 return result;
}); // ... and so on!

All of this should also reveal why we can’t just return a single, new promise each iteration. Because the loop runs synchronously, each promise will be fired immediately, instead of waiting for those created before it.

[1,2,3].reduce( (previousPromise, nextID) => { console.log(`Loop! ${dayjs().format('hh:mm:ss')}`); return new Promise((resolve, reject) => { setTimeout(() => { console.log(`Resolve! ${dayjs().format('hh:mm:ss')}`); resolve(nextID); }, 1000); });
}, Promise.resolve());

In our console:

"Loop! 11:31:20" "Loop! 11:31:20" "Loop! 11:31:20" "Resolve! 11:31:21" "Resolve! 11:31:21" "Resolve! 11:31:21"

Is it possible to wait until all processing is finished before doing something else? Yes. The synchronous nature of reduce() doesn’t mean you can’t throw a party after every item has been completely processed. Look:

function methodThatReturnsAPromise(id) { return new Promise((resolve, reject) => { setTimeout(() => { console.log(`Processing ${id}`); resolve(id); }, 1000); });
} let result = [1,2,3].reduce( (accumulatorPromise, nextID) => { return accumulatorPromise.then(() => { return methodThatReturnsAPromise(nextID); });
}, Promise.resolve()); result.then(e => { console.log("Resolution is complete! Let's party.")
});

Since all we’re returning in our callback is a chained promise, that’s all we get when the loop is finished: a promise. After that, we can handle it however we want, even long after reduce() has run its course.

Why won’t any other Array methods work?

Remember, under the hood of reduce(), we’re not waiting for our callback to complete before moving onto the next item. It’s completely synchronous. The same goes for all of these other methods:

But reduce() is special.

We found that the reason reduce() works for us is because we’re able to return something right back to our same callback (namely, a promise), which we can then build upon by having it resolve into another promise. With all of these other methods, however, we just can’t pass an argument to our callback that was returned from our callback. Instead, each of those callback arguments are predetermined, making it impossible for us to leverage them for something like sequential promise resolution.

[1,2,3].map((item, [index, array]) => [value]);
[1,2,3].filter((item, [index, array]) => [boolean]);
[1,2,3].some((item, [index, array]) => [boolean]);
[1,2,3].every((item, [index, array]) => [boolean]);

I hope this helps!

At the very least, I hope this helps shed some light on why reduce() is uniquely qualified to handle promises in this way, and maybe give you a better understanding of how common Array methods operate under the hood. Did I miss something? Get something wrong? Let me know!

The post Why Using reduce() to Sequentially Resolve Promises Works appeared first on CSS-Tricks.

Understanding the Almighty Reducer

I was recently mentoring someone who had trouble with the .reduce() method in JavaScript. Namely, how you get from this:

const nums = [1, 2, 3]
let value = 0 for (let i = 0; i < nums.length; i++) { value += nums[i]
}

…to this:

const nums = [1, 2, 3]
const value = nums.reduce((ac, next) => ac + next, 0)

They are functionally equivalent and they both sum up all the numbers in the array, but there is a bit of paradigm shift between them. Let’s explore reducers for a moment because they’re powerful, and important to have in your programming toolbox. There are literally hundreds of other articles on reducers out there, and I’ll link up some of my favorites at the end.

What is a reducer?

The first and most important thing to understand about a reducer is that it will always only return one value. The job of a reducer is to reduce. That one value can be a number, a string, an array or an object, but it will always only be one. Reducers are really great for a lot of things, but they’re especially useful for applying a bit of logic to a group of values and ending up with another single result.

That’s the other thing to mention: reducers will not, by their nature, mutate your initial value; rather they return something else. Let’s walk over that first example so you can see what’s happening here. The video below explains:

It might be helpful to watch the video to see how the progression occurs, but here’s the code we’re looking at:

const nums = [1, 2, 3]
let value = 0 for (let i = 0; i < nums.length; i++) { value += nums[i]
}

We have our array (1, 2, 3) and the first value each number in the array will be added to (0). We walk through the amount of the array and add them to the initial value.

Let’s try this a little differently:

const nums = [1, 2, 3]
const initialValue = 0 const reducer = function (acc, item) { return acc + item
} const total = nums.reduce(reducer, initialValue)

Now we have the same array, but this time we’re not mutating that first value. Instead, we have an initialValue that will only be used at the start. Next, we can make a function that takes an accumulator 
and an item. The accumulator is the collected value
 returned in the last invocation that informs the function what the next value will be added to. In this case of addition, you can think of it as a snowball rolling down a mountain that eats up each value in its path as it grows in size by every eaten value.

snowball accumulating values

We’ll use .reduce() to apply the function and start from that initial value. This can be shortened with an arrow function:

const nums = [1, 2, 3]
const initialValue = 0 const reducer = (acc, item) => { return acc + item
} const total = nums.reduce(reducer, initialValue)

And then shortened some more! Implicit returns for the win!

const nums = [1, 2, 3]
const initialValue = 0 const reducer = (acc, item) => acc + item const total = nums.reduce(reducer, initialValue)

Now we can apply the function right where we called it, and we can also plop that initial value directly in there!

const nums = [1, 2, 3] const total = nums.reduce((acc, item) => acc + item,

An accumulator can be an intimidating term, so you can think of it like the current state of the array as we’re applying the logic on the callback’s invocations.

The Call Stack

In case it’s not clear what’s happening, let’s log out what’s going on for each iteration. The reduce is using a callback function that will run for each item in the array. IThe following demo will help to make this more clear. I’ve also used a different array ([1, 3, 6]) because having the numbers be the same as the index could be confusing.

See the Pen showing acc, item, return by Sarah Drasner (@sdras) on CodePen.

When we run this, we’ll see this output in the console:

"Acc: 0, Item: 1, Return value: 1" "Acc: 1, Item: 3, Return value: 4" "Acc: 4, Item: 6, Return value: 10"

Here’s a more visual breakdown:

  1. It shows that the accumulator is starting at our initial value, 0
  2. Then we have the first item, which is 1, so our return value is 1 (0 + 1 = 1)
  3. 1 becomes the accumulator on the next invocation
  4. Now we have 1 as the accumulator and 3 is the item aince it is next in the array.
  5. The returned value becomes 4 (1 + 3 = 4)
  6. That, in turn, becomes the accumulator and the next item at invocation is 6
  7. That results in 10 (4 + 6 = 10) and is our final value since 6 is the last number in the array

Simple Examples

Now that we’ve got that under our belt, let’s look at some common and useful things reducers can do.

How many of X do we have?

Let’s say you have an array of numbers and you want to return an object that reports the number of times those numbers occur in the array. Note that this could just as easily apply to strings.

const nums = [3, 5, 6, 82, 1, 4, 3, 5, 82] const result = nums.reduce((tally, amt) => { tally[amt] ? tally[amt]++ : tally[amt] = 1 return tally
}, {}) console.log(result)

See the Pen simplified reduce by Sarah Drasner (@sdras) on CodePen.

Wait, what did we just do?

Initially, we have an array and the object we’re going to put its contents into. In our reducer, we ask: does this item exist? If so, let’s increment it. If not, add it and set it to 1. At the end, please return the tally count of each item. Then, we run the reduce function, passing in both the reducer and the initial value.

Take an array and turn it into an object that shows some conditions

Let’s say we have an array and we want to create an object based on a set of conditions. Reduce can be great for this! Here, we want to create an object out of any instance of a number contained in the array and show both an odd and even version of this number. If the number is already even or odd, then that’s what we’ll have in the object.

const nums = [3, 5, 6, 82, 1, 4, 3, 5, 82] // we're going to make an object from an even and odd
// version of each instance of a number
const result = nums.reduce((acc, item) => { acc[item] = { odd: item % 2 ? item : item - 1, even: item % 2 ? item + 1 : item } return acc
}, {}) console.log(result)

See the Pen simplified reduce by Sarah Drasner (@sdras) on CodePen.

This will shoot out the following output in the console:

1:{odd: 1, even: 2}
3:{odd: 3, even: 4}
4:{odd: 3, even: 4}
5:{odd: 5, even: 6}
6:{odd: 5, even: 6}
82:{odd: 81, even: 82}

OK, so what’s happening?

As we’re going through every item in the array, we create a property for even and odd, and based on an inline condition with a modulus operator, we’ll either store the number or increment it by 1. The modulus operator is really good for this because it can quickly check for even or odd — if it’s divisible by two, it’s even, if not, it’s odd.

Other resources

At the top, I mentioned other posts out there that are handy resources to get more familiar with the role of reducers. Here are a few of my favorites:

  • The MDN documentation is wonderful for this. Seriously, it’s one of their best posts, IMO. They also describe in a bit more detail what happens if you don’t provide an initial value, which we didn’t cover in this post.
  • Daniel Shiffman is always amazing at explaining things on Coding Train.
  • A Drip of JavaScript does a good job, too.

The post Understanding the Almighty Reducer appeared first on CSS-Tricks.