JavaScript Promise inside async/await function res

2019-08-15 16:16发布

问题:

I'm quite a newbie in JavaScript and in Promises. I'm trying to build an array of objects that I get from an API.

To do so, I've build two functions in a file MyFile.js.

The first one returns a promise when an axios promise is resolved. It's

function get_items (url) {
    return new Promise((resolve, reject) => {
        let options = {
            baseURL: url,
            method: 'get'
        }
        axios(options)
            .then(response => {
                resolve(response.data)
            })
            .catch(error => {
                reject(error.stack)
            })
    })
}

The second one looks like this:

let output = []
let next_url = 'https://some_url.com/api/data'
async function get_data () {
    try {
        let promise = new Promise((resolve, reject) => {
            if (next_url) {
                get_items(next_url)
                    .then(response => {
                        output.push(...response.results)
                        if (response.next) {
                            next_url = response.next
                            console.log('NEXT_URL HERE', next_url)
                            get_data()
                        } else {
                            console.log('else')
                            next_url = false
                            get_data()
                        }
                    })
                    .catch(error => {
                        reject(error.stack)
                    })
            } else {
                console.log('before resolve')
                resolve(output)
            }
        })
        return await promise
    } catch(e) {
        console.log(e)
    }
}

It's where I'm grinding my teeth. What I think I understand of this function, is that:

  • it's returning the value of a promise (that's what I understand return await promise is doing)
  • it's a recursive function. So, if there is a next_url, the function continues on. But if there is not, it gets called one last time to go into the else part where it resolves the array output which contains the results (values not state) of all the promises. At least, when I execute it, and check for my sanity checks with the console.log I wrote, it works.

So, output is filled with data and that's great.

But, when I call this function from another file MyOtherFile.js, like this:

final_output = []
MyFile.get_data()
    .then(result => {
        console.log('getting data')
        final_output.push(...result)
    })

it never gets into the then part. And when I console.log MyFile.get_data(), it's a pending promise.

So, what I would like to do, is be able to make get_data() wait for all the promises result (without using Promise.all(), to have calls in serie, not in parallel, that would be great for performances, I guess?) and then be able to retrieve that response in the then part when calling this function from anywhere else. Keep in mind that I'm really a newbie in promises and JavaScript in general (I'm more of a Python guy).

Let me know if my question isn't clear enough. I've been scratching my head for two days now and it feels like I'm running in circle.

Thanks for being an awesome community!

回答1:

There are a few problems. One is that you never resolve the initial Promise unless the else block is entered. Another is that you should return the recursive get_data call every time, so that it can be properly chained with the initial Promise. You may also consider avoiding the explicit promise construction antipattern - get_items already returns a Promise, so there's no need to construct another one (same for the inside of get_items, axios calls return Promises too).

You might consider a plain while loop, reassigning the next_url string until it's falsey:

function get_items (baseURL) {
  const options = {
    baseURL: url,
    method: 'get'
  }
  // return the axios call, handle errors in the consumer instead:
  return axios(options)
    .then(res => res.data)
}

async function get_data() {
  const output = []
  let next_url = 'https://some_url.com/api/data'
  try {
    while (next_url) {
      const response = await get_items(next_url);
      output.push(...response.results)
      next_url = response.next;
    }
  } catch (e) {
    // handle errors *here*, perhaps
    console.log(e)
  }
  return output;
}

Note that .catch will result in a Promise being converted from a rejected Promise to a resolved one - you don't want to .catch everywhere, because that will make it difficult for the caller to detect errors.



回答2:

This is a bit untested

const api_url = 'https://some_url.com/api/data';

get_data(api_url).then((results) => {
  console.log(results);
}).catch((error) => {
   // console.error(error);
});

function get_items (url) {
  const options = {
    baseURL: url,
    method: 'get'
  };

  return axios(options).then((response) => response.data);
}

async function get_data(next_url) {
  const output = [];

  while (next_url) {
    const { results, next } = await get_items(next_url);
    output.push(...results);
    next_url = next;
  }

  return output;
}

Basically it makes things a bit neater. I suggest to look at more examples with Promises and the advantage and when to ease await/async. One thing to keep in mind, if you return a Promise, it will follow the entire then chain, and it will always return a Promise with a value of the last then.. if that makes sense :)



回答3:

Another way of doing it is to not use async at all and just recursively return a promise:

const getItems = (url) =>
  axios({
    baseURL: url,
    method: 'get',
  }).then((response) => response.data);

const getData = (initialUrl) => {
  const recur = (result, nextUrl) =>
    !nextUrl
      ? Promise.resolve(result)
      : getItems(nextUrl).then((data) =>
          recur(result.concat([data.results]), data.next),
        );
  return recur([],initialUrl)
    .catch(e=>Promise.reject(e.stack));//reject with error stack
};

As CertainPerformance noted; you don't need to catch at every level, if you want getData to reject with error.stack you only need to catch it once.

However; if you had 100 next urls and 99 of them were fine but only the last one failed would you like to reject in a way that keeps the results so far so you can try again?

If you do then the code could look something like this:

const getData = (initialUrl) => {
  const recur = (result, nextUrl) =>
    !nextUrl
      ? Promise.resolve(result)
      : getItems(nextUrl)
        .catch(e=>Promise.reject([e,result]))//reject with error and result so far
        .then((data) =>
          recur(result.concat([data.results]), data.next),
        );
  return recur([],initialUrl);//do not catch here, just let it reject with error and result
};