1

So I have this code:

  for (let i = 0; i < array.length; i++) {
    let url = array[i];
    YTDL.getInfo(url, function(err, info) {
      if (err) {
        message.channel.send("There was an error while checking information about a video, try again soon.");
        throw err;
      }
      songEmbed.addField("Title:", info.title);
      if (i == array.length - 1) message.channel.send(songEmbed);
    });
  }

And the problem is that the function inside YTDL.getInfo() is called after the for loop has ended, but I need to call message.channel.send(songEmbed) only after the last iterate (see code), I tried my best to solve this by myself, using array.forEach() for example, but I cant figure out how to pass the right element index to this function inside YTDL.getInfo() I hope you understand me.

AnB
  • 119
  • 3
  • 10
  • @kemicofa I suspect because `getInfo` is asynchronous. – Mark Jan 07 '19 at 14:22
  • @kemicofa Because all the YTDL.getInfo() callbacks that are adding a field to songEmbed are not called yet back then. – AnB Jan 07 '19 at 14:23

5 Answers5

0

It takes time to get a response from a single url, let alone the multiple url's in your array. This is an async call, so to wait for a response you should use Promises. A lot of examples are available out there. This should be helpful https://medium.freecodecamp.org/javascript-from-callbacks-to-async-await-1cc090ddad99

Moez ‌
  • 61
  • 5
0

Try to understand the basics of sync/async in javascript. There are the most popular resources for this purpose:

https://www.youtube.com/watch?v=cCOL7MC4Pl0

https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

https://www.youtube.com/watch?v=8aGhZQkoFbQ

Your mistake is running the async function from the sync for-loop, as a result even inside the first run of the function you'll get the index of the last item. Spend time with links I've sent you and this topic, it will help you a lot.


What about your question and solution for it just use async for loop, links for getting familiar:

https://hackernoon.com/async-await-essentials-for-production-loops-control-flows-limits-23eb40f171bd

https://codeburst.io/asynchronous-code-inside-an-array-loop-c5d704006c99

Yevhenii Herasymchuk
  • 2,047
  • 15
  • 20
0

What you are experiencing is one of the most frustrating/powerful things with javascript, asynchronous methods. I'm guessing in the getInfo method you are making some form of ajax call out. Javascript, to save time and resources, will let the call fall to the background so that it does not block the next code from running. This is normally good as it allows lots of javascript to run at once and the page loads much faster than it would if it were synchronous. But for you, that means that when accessing info.title, it has not actually been set. The simple and dirty way to fix this is to add a setTimeout call around the stuff you want to be called in the for loop. Something like:

setTimeout(function(){ 
      if (err) {
        message.channel.send("There was an error while checking information about a video, try again soon.");
        throw err;
      }
      songEmbed.addField("Title:", info.title);
      if (i == array.length - 1) message.channel.send(songEmbed); 
}, 3000);

There are better ways of handling this issue as some other answers have pointed out but this should demonstrate the issue a bit better.

Daniel Black
  • 550
  • 6
  • 13
0

Because of the code runs asynchronously the loop ends before the function is finished.

You can either use promises or callbacks to keep it simple. If you don't know either, I recommend you start using callback, and when you can master it, start using Promises and Async and Await.

An example of how you could do it was to insert your code inside a function, with a callback as a parameter.

function magic_func(array, callback) {
      for (let i = 0; i < array.length; i++) {
        let url = array[i];
        YTDL.getInfo(url, function(err, info) {
          if (err) {
            message.channel.send("There was an error while checking information about a video, try again soon.");
            throw err;
          }
          songEmbed.addField("Title:", info.title);
          if (i == array.length - 1) {
            message.channel.send(songEmbed)
            callback()
          }
        });
      }
}

When the callback is called, you can continue doing your next step, eg:

magic_func(array, function() {
  //ADD your next code here
})
crellee
  • 855
  • 1
  • 9
  • 18
0

According to npm examples getinfo returns a promise. Solution with Promise.all, Array#map and Array#forEach

Promise.all ensures that the then callback will be evoked when every request has completed.

values represents an array of the info's of every request.

Promise.all(array.map(url => YTDL.getInfo(url).catch(e => e))
.then(results=>{
  //all the requests have ended
  //add all titles to songEmbed
  results.forEach(result=>{
     if(result instanceof Error){
        console.warn(result.message);
     } else {
       songEmbed.addField("title:", result.title)
     }
  });
  //send songEmbed
  message.channel.send(songEmbed)
}).catch(err=>console.warn(err.message));
kemicofa ghost
  • 16,349
  • 8
  • 82
  • 131
  • Is it possible to catch an error using Promise like that? For example if one of the videos is unavailable. – AnB Jan 09 '19 at 12:55
  • @AnB Yes of course. Just add the `catch` at the end. However, it's an all or nothing. It means that if one of the requests fail everything fails. Alternatively, you can check out this question which is probably what you're looking for: https://stackoverflow.com/questions/30362733/handling-errors-in-promise-all . I'll also add the solution to my answer in a moment. – kemicofa ghost Jan 09 '19 at 13:02
  • 1
    Big t​​hank​s for this! – AnB Jan 09 '19 at 13:20