loading...
Cover image for Tip - Chain Promises synchronously

Tip - Chain Promises synchronously

marwaneb profile image Marwan El Boussarghini ・4 min read

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.

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.

Practical example

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

// 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.

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).

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.

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:

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. 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").

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.

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

Final solution

// :: 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).

Posted on by:

marwaneb profile

Marwan El Boussarghini

@marwaneb

Full stack Engineer at Third Bridge - London UK 🇬🇧 From Rennes, France 🇫🇷 CentraleSupelec Alumni 🎓 Metal music enthusiast 🤘🏽

Discussion

markdown guide
 

Great article explaining on how to process promises with reduce.

But I missunderstood the title for converting async code to sync code. 😅

 

Good point, thank for the feedback ! Do you think "Chain promises synchronously" would make more sense 😅 ?