0

I have found out how to pass additional variables to the promise .then function by using Promise.all. But how can I pass these variables to the .catch function?

My abstracted code:

// global variable url
var url = [url1, url2, url3];

for (i = 0, i < url.length, i++) {
    // run a promise.all to pass some additional variables to the next .then
    Promise.all([xhr(url[i]), i]).then(function([response, i]) {
        // do something with the response and variable i
        alert(url[i] + " responded: " + response);

    }).catch(function(e) {
        // I need variable i also to be available here!
        alert(url[i] + " failed to respond");

    });
}

function xhr(url) {
    return new Promise(function(resolve, reject) {
        // xmlhttprequest with url
        // resolves with the response
        // or rejects with an error message

    });
}

This is just a very abstract code to explain that I need variable i also to be available in the .catch function.

How to achieve this?

EDIT:

In response to the answers and my comments to them, here is my complete code with using let in the for loop. I think now it is a very neat use of promises to search different search engines in parallel and use the first useful hit. I use Promise.any - an inverted Promise.all - to get the first resolved promise from an array of promises. And I use the for loop to prepare the promises for the Promise.any. nzbDonkeySettings is a global variable with my settings:

// modifiy the promise prototype to add function "any"
// from https://stackoverflow.com/questions/39940152/get-first-fulfilled-promise
Promise.prototype.invert = function() {
    return new Promise((res, rej) => this.then(rej, res));
};
Promise.any = function(ps)  {
    return Promise.all(ps.map((p) => p.invert())).invert();
};

// function to search and download the nzb file
function searchNZB(nzbHeader) {

    // prepare the promises for all search engines
    var getNZB = []; // the promise array
    for (let i = 0; i < nzbDonkeySettings.searchengines.length; i++) {
        if (nzbDonkeySettings.searchengines[i].active) { // but only for "active" search engines
            getNZB[i] = new Promise(function(resolve, reject) {

                // first search for the nzb header
                var nzbSearchURL = nzbDonkeySettings.searchengines[i].searchURL.replace(/%s/, encodeURI(nzbHeader));
                var options = {
                    "url": nzbSearchURL,
                    "responseType": "text",
                    "timeout": 20000
                };
                xhr(options).then(function(response) {
                    // if we have a response, check if we have a result
                    var re = new RegExp(nzbDonkeySettings.searchengines[i].searchPattern, "i");
                    if (re.test(response)) {
                        // if we have a result, generate the url for the nzb file
                        var nzbDownloadURL = nzbDonkeySettings.searchengines[i].downloadURL.replace(/%s/, response.match(re)[nzbDonkeySettings.searchengines[i].searchGroup]);
                        // and download the nzb file
                        var options = {
                            "url": nzbDownloadURL,
                            "responseType": "text",
                            "timeout": 120000
                        };
                        xhr(options).then(function(response) {
                            // if we have a response, check if it is a nzb file
                            if (response.match(/<nzb.*>/i)) {
                                // if it is a nzb file, resolve with the nzb file
                                resolve(response);
                            } else {
                                // if it is not a nzb file, reject
                                reject(new Error(nzbDonkeySettings.searchengines[i].name + ": " + "the downloaded file is not a valid nzb file"));    
                            }
                        }).catch(function(e) {
                            // if the download failed, reject
                            reject(new Error(nzbDonkeySettings.searchengines[i].name + ": " + "an error occurred while trying to download the nzb file"));  
                        });
                    } else {
                        // if we have no result, reject
                        reject(new Error(nzbDonkeySettings.searchengines[i].name + ": " + "no nzb file found"));
                    }   
                }).catch(function(e) {
                    // if we have no response, reject
                    reject(new Error(nzbDonkeySettings.searchengines[i].name + ": " + "an error occurred while searching for the nzb file"));   
                });

            });
        }
    }

    // return a promise
    return new Promise(function(resolve, reject) {

        // lets "race" the promises and get the result from the first resolved promise
        Promise.any(getNZB).then(function(nzbFile) {
            // resolve with the nzbFile
            resolve(nzbFile);
        }).catch(function(e) {
            // if we have no results, reject
            reject(new Error("no search engine returned any usable result"));   
        });     

    });

}
Tensai
  • 50
  • 1
  • 8

3 Answers3

1

Use let like this:

for (let i = 0, i < url.length, i++) {

let creates a separate variable i for each iteration of the for loop so even though the for loop has continued to run while your xhr() requests are running, each invocation of the loop will still have access to its own copy of i.


Also, there's no reason to use Promise.all() at all inside the loop. You only have one xhr() promise for each iteration of the loop:

// global variable url
var url = [url1, url2, url3];

for (i = 0, i < url.length, i++) {
    // run a promise.all to pass some additional variables to the next .then
    xhr(url[i]).then(function([response, i]) {
        // do something with the response and variable i
        alert(url[i] + " responded: " + response);

    }).catch(function(e) {
        // I need variable i also to be available here!
        alert(url[i] + " failed to respond");

    });
}

Now, if you wanted to know when all the xhr() calls were done, you could do this:

// global variable url
var url = [url1, url2, url3];

Promise.all(url.map((u, i) => {
    return xhr(u).catch(err => {
        console.log(u + " failed to respond");
        return null;
    });
})).then(results => {
    console.log(results);
}).catch(err => {
    console.log(err);
});
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • WOW! This is so simple and I even do not need the Promise.all trick anymore! Thank you so much! – Tensai Mar 03 '18 at 20:51
1

I'm not really fond of the idea of adding extra data to promises in this way. You can simply restructure your code to make use of forEach and scoping to reach the desired effect:

const urls = [url1, url2, url3];
urls.forEach(url => {
    xhr(url)
        .then(response => alert(url + "responded: " + response))
        .catch(() => alert(url + "failed to respond"))
})
Peeke Kuepers
  • 193
  • 10
  • Thanks also for your answer, but my actual code is much more complicated then my abstract example (see my edited question) and I don't think that .forEach would help me to simplify the code. – Tensai Mar 03 '18 at 21:30
1

This might be another way to solve your problem;

var apiCall = url => new Promise((v,x) => Math.random() < 0.67 ? v(`some data from ${url}`) : x("Oops..!")),
    urls    = ["url1","url2","url3"],
    ps      = urls.map((u,i) => apiCall(u).then(v => console.log(`Result from API call # ${i+1} is ${v}`))
                                          .catch(e => console.log(`Received Error ${e} from API call ${i+1}`)));
Redu
  • 25,060
  • 6
  • 56
  • 76