4

I've written a short script using Async/Await that prints out letters one by one after short intervals. Based on what I understood to be happening, I tried rewriting the code in several ways expecting the same result, but I have been unable to make any of these alternatives work. In particular, I thought it would be straightforward to change where in the code the console.log() happens.

Here's the original working code:

const welcomeMessage = () => {
  
  const message = 'hello'
  const timer = [200,400,200,400,200,400];

// Promisify setTimeout() and feed in counter from sendMessage()
  const setTimeoutPromise = num => {
    return new Promise(resolve => {
      setTimeout(resolve, timer[num]);
    })
  };

// Async/Await with a For loop calling setTimeoutPromise()
  const sendMessage = async () => {
    for (count = 0; count < message.length; count++) {
      await setTimeoutPromise(count);
      console.log(message[count]);
    };
  };

  sendMessage();

}

welcomeMessage();

Then I tried to make a few modifications, none of which worked.

Mdofication #1: In this version, I thought I could just call and run the code in the sendMessage() function directly without needing to call it later. However, nothing happened after this modification:

async () => { //No name and removed call to sendMessage() later in code
  for (count = 0; count < message.length; count++) {
    await setTimeoutPromise(count);
    console.log(message[count]);
  };
};

const welcomeMessage = () => {
  
  const message = 'hello'
  const timer = [200,400,200,400,200,400];

// Promisify setTimeout() and feed in counter from sendMessage()
  const setTimeoutPromise = num => {
    return new Promise(resolve => {
      setTimeout(resolve, timer[num]);
    })
  };

  async () => { //No name and removed call to sendMessage() later in code
    for (count = 0; count < message.length; count++) {
      await setTimeoutPromise(count);
      console.log(message[count]);
    };
  };
}

welcomeMessage();

Modification #2: I reverted the code and then tried to move the console.log() function into the setTimeout() function thinking this would be called on every loop. Both with empty ()'s and with (resolve) being passed into setTimeout(), it only printed the first letter. With (resolve, num) it says undefined:

  const setTimeoutPromise = num => {
    return new Promise(resolve => {
      setTimeout((resolve) => {
          console.log(message[num]);
          resolve;
      }, timer[num]);
    })
  };

  const sendMessage = async () => {
    for (count = 0; count < message.length; count++) {
      await setTimeoutPromise(count);
    };
  };

const welcomeMessage = () => {
  
  const message = 'hello'
  const timer = [200,400,200,400,200,400];

  const setTimeoutPromise = num => {
    return new Promise(resolve => {
      setTimeout((resolve) => {
          console.log(message[num]);
          resolve;
      }, timer[num]);
    })
  };

  const sendMessage = async () => {
    for (count = 0; count < message.length; count++) {
      await setTimeoutPromise(count);
    };
  };

  sendMessage();

}

welcomeMessage();

Modification #3: Finally, I tried to define a function in advance to be passed into setTimeout() which would be used to handle "resolve" and console.log(). I tried a few variations and again didn't seem to be progressing through the loop as console.log() was only called once.

  // New function to handle resolve and the counter
  function newFunction(func, num) {
    console.log(message[num]);
    func;
  }

  const setTimeoutPromise = num => {
    return new Promise(resolve => {
      setTimeout(newFunction(resolve, num), timer[num]);
    })
  };

  const sendMessage = async () => {
    for (count = 0; count < message.length; count++) {
      await setTimeoutPromise(count);
    };
  };

const welcomeMessage = () => {
  
  const message = 'hello'
  const timer = [200,400,200,400,200,400];

  // New function to handle resolve and the counter
  function newFunction(func, num) {
    console.log(message[num]);
    func;
  }

  const setTimeoutPromise = num => {
    return new Promise(resolve => {
      setTimeout(newFunction(resolve, num), timer[num]);
    })
  };

  const sendMessage = async () => {
    for (count = 0; count < message.length; count++) {
      await setTimeoutPromise(count);
    };
  };
  
  sendMessage()

}

welcomeMessage();
FZs
  • 16,581
  • 13
  • 41
  • 50
  • 2
    `func;` is not a function call (compare to existing function calls like `setTimeout(…)` – it needs parentheses). `async () => {}` is a function expression that is, too, missing a call. See https://stackoverflow.com/questions/8228281/what-is-the-function-construct-in-javascript. – Ry- May 20 '20 at 06:31
  • 2
    `resolve;` has the same problem. It should be `resolve()`. At `setTimeout(newFunction(resolve, num), timer[num])`, for the correct use of `setTimeout`, see https://stackoverflow.com/questions/7137401/why-is-the-method-executed-immediately-when-i-use-settimeout. – Ry- May 20 '20 at 06:34
  • 2
    In last code you have error you call function newFunction and pass nothing to setTimeout you need to write function that return function or wrap it inline `setTimeout(() => newFunction(resolve, num), timer[num]);` and you need to call that function `func`. – jcubic May 20 '20 at 07:50
  • 1
    Thanks Ry- for your replies and the useful links. I was able to get the code running and gained a bit more understanding about the implications of my code. @jcubic, thanks as well for the explanation. – UnderwaterHandshake May 20 '20 at 14:19

1 Answers1

1

It appears to me, that you've started working with asynchrony before you got a deep understanding of how synchronous JavaScript works. Asynchrony is hard enough on its own too, so combined to that, it made you completely confused.

Let me explain what's going on and what's wrong in your snippets.


Let's start with the working one.

That code:

const setTimeoutPromise = num => {
  return new Promise(resolve => {
    setTimeout(resolve, timer[num]);
  })
};

...creates a function named setTimeoutPromise, that:

  • takes an index (number) as its argument
  • returns a promise that:
    • after timer[num] milliseconds
    • resolves to undefined (setTimeout doesn't pass anything to its callback by default; in this case the callback is the resolve function)

The next part:

const sendMessage = async () => {
  for (count = 0; count < message.length; count++) {
    await setTimeoutPromise(count);
    console.log(message[count]);
  };
};

...defines an async function named sendMessage, that:

  • iterates over message, for each character:
    • calls setTimeoutPromise and awaits the promise it returns
    • after waiting, logs the current character to the console

Finally,

sendMessage();

...calls sendMessage, and therefore initiates the typing.


Now, let's move on to the next snippet.

This code:

async () => { //No name and removed call to sendMessage() later in code
  for (count = 0; count < message.length; count++) {
    await setTimeoutPromise(count);
    console.log(message[count]);
  };
};

...creates an async function, but it doesn't call or assign it to any variable: simply discards it.

To fix this snippet, call the function immediately by putting () after it!

const welcomeMessage = () => {
  
  const message = 'hello'
  const timer = [200,400,200,400,200,400];

// Promisify setTimeout() and feed in counter from sendMessage()
  const setTimeoutPromise = num => {
    return new Promise(resolve => {
      setTimeout(resolve, timer[num]);
    })
  };

  (async () => { //No name and removed call to sendMessage() later in code
    for (count = 0; count < message.length; count++) {
      await setTimeoutPromise(count);
      console.log(message[count]);
    };
  })(); //The () at the end calls it
}

welcomeMessage();

Problematic snippet #2

There are 2 problems with this:

const setTimeoutPromise = num => {
  return new Promise(resolve => {
    setTimeout((resolve) => { //Problem 1
        console.log(message[num]);
        resolve; //Problem 2
    }, timer[num]);
  })
};
  1. You try to take an argument named resolve from setTimeout, but as I mentioned above, it doesn't pass any.

    To solve it, remove resolve from setTimeout((resolve) => {! We already have the resolve function from the above line, because of lexical scope.

  2. You don't call resolve, that keeps the awaiting code hanging after the first letter (the promise never gets resolved).

    To fix it, put () after resolve!

const welcomeMessage = () => {
  
  const message = 'hello'
  const timer = [200,400,200,400,200,400];

  const setTimeoutPromise = num => {
    return new Promise(resolve => {
      setTimeout(() => {
          console.log(message[num]);
          resolve();
      }, timer[num]);
    })
  };

  const sendMessage = async () => {
    for (count = 0; count < message.length; count++) {
      await setTimeoutPromise(count);
    };
  };

  sendMessage();

}

welcomeMessage();

Problematic snippet #3

There are 2 problems in this code as well:

// New function to handle resolve and the counter
function newFunction(func, num) {
  console.log(message[num]);
  func; //Problem 1
}
 const setTimeoutPromise = num => {
  return new Promise(resolve => {
    setTimeout(newFunction(resolve, num), timer[num]); //Problem 2
  })
};
  1. The same as above; newFunction doesn't call resolve (named fn).

    Try to not forget the () when you intend to call a function

  2. That's the opposite of Problem 1. You immediately call newFunction (due to the parentheses after it: (resolve, num)), and pass its return value (undefined) to the setTimeout. Without Problem 1, this would result in immediately logging all letters.

    In this case, let setTimeout to call that function internally by removing (resolve, num) after it. To pass parameters to it, setTimeout accepts additional arguments, that it will hand over to its callback (in this case newFunction).

const welcomeMessage = () => {
  
  const message = 'hello'
  const timer = [200,400,200,400,200,400];

  // New function to handle resolve and the counter
  function newFunction(func, num) {
    console.log(message[num]);
    func();
  }

  const setTimeoutPromise = num => {
    return new Promise(resolve => {
      setTimeout(newFunction, timer[num], resolve, num);
    })
  };

  const sendMessage = async () => {
    for (count = 0; count < message.length; count++) {
      await setTimeoutPromise(count);
    };
  };
  
  sendMessage()

}

welcomeMessage();

All together...

It is possible to combine these fixes, to get something like:

const welcomeMessage = () => {
  
  const message = 'hello'
  const timer = [200,400,200,400,200,400];

  // New function to handle resolve and the counter
  function newFunction(func, num) {
    console.log(message[num]);
    func();
  }

  const setTimeoutPromise = num => {
    return new Promise(resolve => {
      setTimeout(newFunction, timer[num], resolve, num);
    })
  };

  (async () => {
    for (count = 0; count < message.length; count++) {
      await setTimeoutPromise(count);
    };
  })();

}

welcomeMessage();

Conclusion

Use parentheses (()) to call a function, but avoid them to use the function as an object: pass or assign it to something, get or set its properties, etc.

Community
  • 1
  • 1
FZs
  • 16,581
  • 13
  • 41
  • 50
  • 1
    Incredible answer. Thanks. Going to be coming back to this one over the next couple days to make sure I'm fully digesting this and to do some follow up reading and tinkering. First time posting a question on the site -- going to be spoiled after this one. – UnderwaterHandshake May 20 '20 at 14:34
  • @UnderwaterHandshake I'm glad that I could help. Your question is a relatively good one here (just read the 10 newest questions tagged JavaScript and compare to yours), especially as a first question. And I think good questions deserve a good answer. And... welcome to SO! – FZs May 20 '20 at 15:05
  • I've been rereading the answer, and the one point I'm struggling to comprehend is in Snippet #3 -> your point #2. You point out that I am immediately calling `newFunction` and return `undefined` which will lead to all letters logging at once. I believe I encountered this myself, but I don't understanding why. I see two potential reasons: – UnderwaterHandshake May 20 '20 at 15:43
  • 1
    It is because you essentially cancel out the effect of `setTimeout`. It's the same as if you wrote `const returnValue = newFunction(resolve, num); setTimeout(returnValue, timer[num]);`, where `retrunValue` is `undefined` – FZs May 20 '20 at 15:49
  • Does this mean if the first parameter of `setTimeout` is undefined it never proceeds to start the timer? – UnderwaterHandshake May 20 '20 at 15:52
  • 1
    I wrote about what happens in this case [in one of my older answers](https://stackoverflow.com/a/55050058/8376184). That's a common mistake, that almost everybody makes who are new to JS. 3 years ago, I made the same mistake and didn't understood why it didn't work... – FZs May 20 '20 at 15:58
  • [Moved the discussion into chat. Continue there!](https://chat.stackoverflow.com/rooms/214273/discussion-between-fzs-and-underwaterhandshake) – FZs May 20 '20 at 16:03