Tip - Chain Promises synchronously
Tags: javascript, functional, declarative
Published: 2019-10-30 09:50:01



On one my company's project, we faced a non-trivial issue that I find could be really interesting to solve together. This article will basically give you an overview of the problem and how I solved it using the Array.prototype method `reduce`.

Here is a quick overview of the context around my issue.

I needed for one of my projects to create a state machine that would asynchronously process a string character by character.


```js
const initialState = { /* Initial state of the state machine */ };

// processCharacter :: (State) -> (character) -> Promise(State)
const processCharacter = (state) => async (character) => {
  // ...asynchronously process the character
  return newState;
};

// (State) -> Promise(Result)
const getResultFromState = (state) => {
  // ...converts the state to a result
  return result;
}

// Function to implement.
// :: string -> Promise(Result)
const processString = async (string) => {
  // ?
  return result
}
```

In a nutshell, here are the different step given an initialState:

- Split the string into an array of characters.
- For each `character`, call `await processCharacter(character)(state)` and assign it to `state`.
- Call `getResultFromState` to the last version of the `state`.

__TL;DR__ Go straight to the solution by clicking [here](#final-solution).

## Practical example

To illustrate this problem, let's define a practical example:

```js
// Simulates different processing time based on character.
const calculateTimeout = character => character.charCodeAt(0) * 10;
// Allows to set to use setTimeout with await.
const timeout= async (ms) => new Promise(rs => setTimeout(rs, ms));
// :: string -> [character]
const stringToArray = string => [...string];

const initialState = { stringProcessed: '' };

const processCharacter = ({ stringProcessed }) => async (character) => {
  const ms = calculateTimeout(character);
  await timeout(ms);
  return { stringProcessed: `${stringProcessed}${character}`};
};

const getResultFromState = ({ stringProcessed }) => {
  return stringProcessed;
}

const processString = async (string) => {
  // ?
  return result
}
```

## Solution

Let me show you two approaches to the problem: one purely imperative - which does makes sense but lacks of elegance - and a far more concise solution.

---------

### First approach: imperative way

With an imperative approach, we want to describe each step of the function as it is describe in problem.

```js
const processString = async (string) => {
  let state = initialState;
  const stringArray = stringToArray(string);
  for (let i = 0; i < stringArray.length; i++) {
    state = await processCharacter(state)(stringArray[i]);
  }
  return getResultFromState(result);
}
```

To be fair, this works and it will return you exactly what you're looking for.
I still have several issues with this code:

- I like the functional programming approach and re-assigning `state` at each step kills me a bit more every time,
- the old fashioned `for` loop is extremely verbose and I don't think I've used it since I learned how to use `map` and `forEach`.

_"But Marwan, why can't we use a `forEach` ?"_ you may ask.

Let's try (and forget my first point, nvm).

```js
const processString = async (string) => {
  let state = initialState;
  stringToArray(string).forEach(async (character) => {
    state = await processCharacter(state)(stringArray[i]);
  });
  return getResultFromState(result);
}
```

Brilliant! But if you run this in a gist for the string `"Test"`, you receive ... `""`. Wait what?

Indeed... This will not work because of the definition of `forEach`: you could basically reimplement it as below.

```js
Array.prototype.forEach = function(fn) {
  for(let i = 0; i < this.length; i++) {
    fn(i);
  }
}
```

Do you see where the issue comes from?
Yes! From the `await`!
In fact this particular point differentiate `forEach()` and our good old for loop.
If you wanted to make that work, you would probably want to start the code by this:

```js
Array.prototype.forEach = function(fn) {
  for(let i = 0; i < this.length; i++) {
    await fn(i);
  }
}
```

But please don't that...

Instead let run you through a more elegant solution.

---------

### Using reduce()

For those who have read some articles on my blog, you might know about my fascination for the `reduce()` function and [how powerful it can be](https://dev.to/marwaneb/toolkit-map-filter-reduce-in-arrays-4mp7#reduce-sometimes-simple-is-not-enough). Here is one the example of why I think so.

Think of the problem like this: what we want to do can be also written as below (let's take the string `"monster"`).

```js
processCharacter(initialState)('m')
  .then(state => processCharacter('o')(state))
  .then(state => processCharacter('n')(state))
  .then(state => processCharacter('s')(state))
  .then(state => processCharacter('t')(state))
  .then(state => processCharacter('e')(state))
  .then(state => processCharacter('r')(state));
```

This way to iterate a pattern over a number of element usually pushes me toward `reduce()`.

Let's define our reducer: we basically want our accumulator and for each character to apply processCharacter.

```js
const reducer = (accumulator, character) => accumulator.then(state => processCharacter(character)(state));
```

As our initial value, we still want to have a Promise to be able to apply `.then` to it. Here it is: `Promise.resolve(initialState)` that is basically a promise that will resolve our initial state and start to make our dominoes fall.

![Here fall the dominoes](https://thepracticaldev.s3.amazonaws.com/i/mmy67hdlasm97jts2qit.gif)

### Final solution

```js
// :: string -> [character]
const stringToArray = string => [...string];
const processString = async (string) => getResultFromState(
  stringToArray(string).reduce(
    (acc, character) => acc.then((state) => processCharacter(state)(character)),
    Promise.all(initialState),
  )
);
```

I hope you enjoyed reading this article!

I really wanted to highlight the way I think about a problem and try to solve it and to give you a glance of the beauty of declarative programming (that I will probably cover in a future article).