0

TL;DR

Can't get an async/await function to do what an async function achieves by returning a custom new Promise object.


I'm trying to build a function that takes a string, loops through the words inside the string, and set an interval that logs each word after the set interval you assigned. Once it's done logging, a callback will log the total amount of words after the function has finish logging each of them. Below, you have the main function.

async function textReader (text, callback, interval = 1000) {
  return new Promise((resolve, reject) => {
    let counter = 0
    let textArray = text.trim().split(' ')
    let idInterval = setInterval( () => {    

      if(counter == textArray.length) {
        callback(textArray.length)
        clearInterval(idInterval)
        resolve();

      } else {
        console.log(textArray[counter++])
      }
    }, interval)
  })
}

Then, the callback that logs the quantity of words displayed:

function funCallback (wordQuantity) {
  console.log(`Process complete - The text contains ${wordQuantity} words`)
}

And finally, an async function that tests the main function. It simply runs 3 times the main function and log each of them, one after the other, as it is supposed to be. The idea is that each await blocks the process until there is a resolved value (which actually means: while it is logging in the terminal each word), and once that await is done, jump to the next textReader function and so on.

async function test () {
  try {
    let time = 500
    await textReader('When I find myself in times of trouble ', funCallback, time)
    await textReader('mother Mary comes to me ', funCallback, time)
    await textReader('speaking words of wisdom Let it be', funCallback, time)
  } catch (err) { console.log(err)}
}

My issue is that I want the textReader function to be able to achieve the same behavior without having to return a new Promise, but using await (because I guess that's what an async/await function should be helpful with, right? achieve the same as an ES6 Promise)

Please have in mind that the goals of the whole program are to:

  • Log the words in a specific interval
  • If test() holds more than one textReader(), they have to be blocking, meaning that one has to wait for the other to finish logging its words, otherwise the words from all the tested functions would overlap one over the other - which would be very confusing BTW -.
  • Count how many words were logged in each string.

I just don't see how can it be solved without having to return from textReader() a new Promise but using await, as it should be in a async/await function.

One of the attempts to solve it with async/await (see below) didn't work; it just runs the 3 textReader() from test() functions all at the same time, overlapping the logs.

async function textReader (text, callback, time = 1000) {
  let counter = 0
  let textArray = text.trim().split(' ')
     
  let loggingWords = async () => {
        let idInterval = setInterval( () => {    
    
          if(counter == textArray.length) {
            callback(textArray.length) 
            clearInterval(idInterval)     
          } else { 
            console.log(textoArray[contador++]) 
          } 
        }, time) 
      }

      let waitForLogingWords = await loggingWords()
      
      return waitForLogingWords
    };
  • You cannot use `setInterval` with promises. Put a loop around an `await`ed `delay` function (that returns a `new Promise` for `setTimeout` - [you can't completely avoid `new Promise` there](https://stackoverflow.com/q/45788934/1048572)). – Bergi Jan 25 '21 at 18:17
  • *"because I guess that's what an async/await function should be helpful with, right? achieve the same as an ES6 Promise"* - No. `async`/`await` *hides* the promises. It makes it *look like* there weren't any promises. It does not make the promises go away. Everything `async`/`await` does is powered by promises behind the scenes. `async`/`await` **is** Promises. Avoiding `new Promise()` does not per se make your code better, using it does not per se make it worse. – Tomalak Jan 25 '21 at 19:29
  • What you should avoid though, are `callback` arguments. That's the whole point of why Promises exist in the first place - to decouple the function that executes a task from the task consumer. Passing a callback from the consumer into the worker breaks that paradigm. – Tomalak Jan 25 '21 at 19:31

1 Answers1

0

As indicated in the comments, there is no way you can fully avoid calls to new Promise(). Once that's established we might as well embrace it.

Here is a generic solution to the "I want to use promises to process a list of items in sequence, with a delay between each item" problem.

const sequence = (iterable, payload, delay=0) =>
  iterable.reduce((p, item) =>
    p.then(() =>
      new Promise(r => setTimeout(r, delay)).then(() => payload(item))
    ), Promise.resolve()
  );

It takes a list of items and creates a promise chain, each promise fulfilling delay milliseconds after the previous one did.

Based on this it's easy to implement your textReader - it's a sequence over words that logs each word:

var textReader = (text, interval=1000) =>
  sequence(text.trim().split(' '), word => console.log(word), interval);

Your function that processes the song again only is a sequence over lines, so it's equally easy to implement. This time as an async function (but that's just details - sequence(...).then(...) would work the same):

async function test() {
  try {
    await sequence([
      'When I find myself in times of trouble ',
      'mother Mary comes to me ',
      'speaking words of wisdom Let it be'
    ], line => textReader(line, 500));
    console.log('-all done-');
  } catch (err) {
    console.log(err);
  }
}

Once we run test(), we get a neatly staggered output of words, each 1/2 second after the other:

When
I
find
myself
in
times
of
trouble
mother
Mary
comes
to
me
speaking
words
of
wisdom
Let
it
be
-all done-
Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • You should still avoid the [`Promise` constructor antipattern](https://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it) in `sequence`, where you are not errors in `p`, or exceptions from `payload`. Put it in a separate `delay` function that does only that, then use proper promise chaining (or `async`/`await`) in `sequence`. – Bergi Jan 25 '21 at 22:37
  • @Bergi The promises above only chain timeouts, they can't error out unless I'm missing something. Catching exceptions in `payload` is the caller's responsibility. – Tomalak Jan 26 '21 at 01:14
  • They can error if `payload()` returns a rejected promise. And no, catching exceptions is the responsibility of the promise chain, *handling* the rejected promise then is the caller's responsibility. – Bergi Jan 26 '21 at 01:22
  • Hm... I would have avoided `new Promise()` if there was a different way of calling `resolve()` with a delay. I'm not aware of any method of doing that *other* than creating a new promise, since only the promise constructor has access to the `resolve()` callback. – Tomalak Jan 26 '21 at 01:24
  • I'm not saying that you can avoid `new Promise` completely, I'm saying that you should not use promises inside the executor. The proper solution would be `array.reduce((p, item) => p.then(() => delay(t)).then(() => callback(item)), Promise.resolve())` with `delay = t => new Promise(res => { setTimeout(res,t); });` – Bergi Jan 26 '21 at 01:32
  • @Bergi Okay, so - sans the separate `delay` function - like in the updated answer? I can see why separating the `delay` part from the payload execution part is cleaner. – Tomalak Jan 26 '21 at 01:50