0

I have a function that calls an API, and the API accepts a callback:

const callApi = async (param1, param2) => {
    api.doSomething(param1, param2, (result) => {
        // I want to return the result as the result of callApi
    }
}

And I have a list/array of objects on which I want to call the Api function, treat the results one by one, and then sort them and pick the one that fits my criteria.

I do NOT want to shove all the logic in the callback. I know I could, but it would result in ugly looking code that will be hard to read 1 year from now.

Something like:

let results = myArrayOfObjects.map(function(myObject){
    callApi(myObject.field1, myObject.field2)
        .then(
            // here I'd like to get the result of api.doSomething
            // do more stuff here with that result
        )

    // return an object here, or a promise

})

// do something with the results after all objects in myArrayOfObjects have been processed
// for example, sort()

If the results are promises, I am thinking of using Promise.all to wait for all of them to complete. Otherwise, it would be even easier to work with the results.

Thank you.

D. Joe
  • 573
  • 1
  • 9
  • 17
  • Does this answer your question? [How to return the response from an asynchronous call](https://stackoverflow.com/questions/14220321/how-to-return-the-response-from-an-asynchronous-call) – Seblor Sep 28 '21 at 11:52
  • 1
    You can [promisify your `doSomething` callback](https://stackoverflow.com/questions/22519784/how-do-i-convert-an-existing-callback-api-to-promises) by wrapping it in a Promise (if your API doesn't already return one), then you can map your array to the result of `callApi` which now returns promises. This will give you an array of Promises that you can then use `Promise.all()` with `.then()` or `await` to extract the array results – Nick Parsons Sep 28 '21 at 12:02
  • @Seblor no, it doesn't. I had already read that (looong) page twice, and it keeps coming up as related to my question, but unfortunately it doesn't do what I want to do. – D. Joe Sep 28 '21 at 12:07
  • @NickParsons could you perhaps post an answer with my code modified to promisify the callback ? I tried putting return statements everywhere, and still I couldn't make it work. I spent already at least a couple of hours on this, but JS is not my main programming language. Thanks. – D. Joe Sep 28 '21 at 12:08

3 Answers3

2

You could first promisify your API so that it returns a Promise that resolves to your result provided by your callback. It might be worth checking your API implementation and seeing if it already returns a Promise (if that's the case, then you can just return that Promise and ditch the callback)

const callApi = (param1, param2) => {
  return new Promise(resolve => { 
    api.doSomething(param1, param2, (result) => {
        resolve(result);
    });
  });
}

Once you have callApi returning a Promise, you can map over your array and call callApi() to fire your API request, as well as to return the Promise. At this point you can extend your promise chain by attached a .then() to it and return a transformed version of your Promise:

const mapped = myArrayOfObjects.map(myObject => {
    return callApi(myObject.field1, myObject.field2)
        .then(result => // result of api.doSomething
            // do more stuff here with that result
            return result2; // return the transformed result (ie: `result2`)
        );
});

The above piece of code can be re-written a little more nicely using async/await isntead:

const mapped = myArrayOfObjects.map(async myObject => {
    const result = await callApi(myObject.field1, myObject.field2);
    // do more stuff here with that result
    return result2; // result2 represents the "mapped"/transformed version of `result`
});

Once you have an array of promises mapping to your desired value, you can use Promise.all() to wait for all Promises to resolve and retrieve your result:

Promise.all(mapped).then(results => {
    // results is an array based on the mapped values
});

Simple Example (using setTimeout to simulate an asynchronous API request):

const callApi = (n) => {
  return new Promise(resolve => {
    setTimeout(() => {
      const result = Math.random() + n;
      resolve(result);
    }, 1000);
  });
}

const mapped = [1, 2, 3].map(async n => {
  const result = await callApi(n);
  const result2 = `Random number is: ${result}`;
  return result2; // result2 represents the "mapped"/transformed version of `result`
});

Promise.all(mapped).then(results => {
  console.log(results);
});
Nick Parsons
  • 45,728
  • 6
  • 46
  • 64
  • 1
    I think I love you. Thank you !!!! This seems to have done exactly what I wanted. Finally I can get my code to have a logical flow instead of a bunch of imbricated functions. – D. Joe Sep 28 '21 at 13:02
0

Instead of using .then(), create a function that does all of the stuff you want it to do and then send that function into callApi as a parameter.

const doStuff = (results) => {
    // Do stuff here
}

callApi(myObject.field1, myObject.field2, doStuff)

And then in the callApi function:

const callApi = async (param1, param2, stuffDoerFunc) => {
    api.doSomething(param1, param2, stuffDoerFunc);
}

Now that I think about it, you could probably even simplify it further:

const doStuff = (results) => {
    // Do stuff here
}

api.doSomething(myObject.field1, myObject.field2, doStuff);
Emil Karlsson
  • 1,000
  • 1
  • 7
  • 16
  • I know I could do that, but I just edited my question to explain that I don't want to shove all the logic in the callback. It would be ugly. It would look horrible. – D. Joe Sep 28 '21 at 11:59
  • I very much disagree. It would neither be ugly, nor horrible. I think your way of doing it is much worse. But you are entitled to have your own taste. Good luck. – Emil Karlsson Sep 28 '21 at 12:02
  • It's not about taste, it's about the fact that I want to do quite a lot with that result, including possibly calling other functions that accept callbacks. Which would result in a function in a function in a function in a function in a function. If you don't think that's ugly .... – D. Joe Sep 28 '21 at 12:04
  • Then you clearly have a lot of code to clean up. That doesn't mean that there is anything wrong with the code I wrote. It just means that you have a lot of messy code in your code base. Fix that code instead. – Emil Karlsson Sep 28 '21 at 12:06
  • What is there to clean if I want to make multiple calls to that API (that I didn't write myself and can't change), each time using the result of the previous call ?? There's nothing wrong with your code as a solution to a different problem. Thanks for trying to help, but perhaps accept that you aren't always right/don't always have the best solution. I know it's hard for most devs to accept that, but it happens. – D. Joe Sep 28 '21 at 12:11
  • I am not trying to be mean, and I apologize if my replies were rude. – Emil Karlsson Sep 28 '21 at 12:20
  • it's all good. I appreciate you trying to help. I understand you don't know what I want to do, and having seen tons of sh*t code myself over 20+ years of dev, I can see why you assumed my code is bad. But what I am trying to do now is NOT writing sh*t code with long functions and imbricated functions. – D. Joe Sep 28 '21 at 12:28
0
// Wrap your object/array processor in a standalone function
function processObjects() {
    let results = myArrayOfObjects.map(function(myObject){
        callApi(myObject.field1, myObject.field2)
            .then(
                // here I'd like to get the result of api.doSomething
                // do more stuff here with that result
            )    
    
        // return an object here, or a promise
    
    })
}

// Wrap your post-processing in a second function
function processResults() {
    // do something with the results after all objects in myArrayOfObjects have been processed
    // for example, sort()
}

// Create a master function to call, which calls both functions in order, 
// and does not let the second one start until the first one completes
async function processObjectsThenResults() {
    await processObjects();
    await processResults();
}

All this wizardry is unnecessary in a sequential functional language, but in the synchronous swamp of JS you need to force it to wait until your first group of commands finishes before starting the post-processing, otherwise it will try to just do it all at once and overlap and mayhem will ensue!

To modify this approach to pass results, you could just push them from one function to the other. A hacky way to do this would be passing the result of the first function into the second, like this:

async function processObjectsThenResults() {
    someArrayVariable = await processObjects();
    await processResults(someArrayVariable);
}

You would need to adjust the second function to be able to receive and interpret the params in the format that the first function outputs them to.

ed2
  • 1,457
  • 1
  • 9
  • 26