function returning too early before filter and reduce finish

…衆ロ難τιáo~ 提交于 2019-12-24 22:16:37

问题


I have the following function, which is supposed to check through an array, sum up some values and then return a string with the sum (for Dialogflow/Google Assitant):

function howMuch(app) {
return bank.getTurnovers(accessToken)
    .then((result) => {
        const { turnovers } = JSON.parse(result);

        let sum = turnovers
            .filter((i) => {
                if (i.tags.includes(tag)) {
                    console.log(i); // correctly prints relevant elements
                    return i;
                }
            })
            .reduce((prev, j) => {
                console.log(prev, j.amount) // correctly prints 0 7, 7 23
                return prev + j.amount;
            }, 0);

        console.log(sum); // prints sum correctly: 30
        return app.ask('You have spend ' + sum); // returns 0 for sum
    })
    .catch((error) => {
        // handle error
    });
};

The problem is however, the function only returns You have spend 0, where 0 is the initial value set in the reduce function, but the sum is actually 30. It just does not seem to wait for the reduce to finish. What is the problem here?


回答1:


app.ask() sends the response back to the user (and, since you're using ask() and not tell(), also says to keep the microphone open). It returns exactly what Express' Response.send() method returns.

It does not return the string to the calling function.

If you want the value, you shouldn't call app.ask() at this point. Just return the value (or a Promise that eventually resolves to that value) and have that call app.ask().




回答2:


Congratulations, you've uncovered the single greatest misconception people have when using Promises: that a Promise can return a predictable value to its calling context.

As we know, callbacks we pass to a new Promise(callback) constructor receive two arguments, each callbacks themselves, that are expected to be invoked with

  1. resolve(result) - the result of the asynchronous operation
  2. reject(error) - an error condition should that operation fail

Let's assume that we define a simple function that returns a string message wrapped in an object:

/**
 * @typedef {Object} EchoResult
 * @property {String} message
 *
 * @example
 *    {message: 'this is a message'}
 */

/**
 * Wraps a message in a EchoResult.
 * @param  {String} msg - the message to wrap
 * @return {EchoResult}
 */
const echo = function (msg) {
  return {
    message: msg
  }
}

By calling

const result = echo('hello from echo!')

console.log(result)

we can expect

{ message: 'hello from echo!'}

to be printed to the console.

So far so good.

Now let's do the same thing wrapped in a Promise:

/**
 * Wraps a msg in an object.
 * @param  {String} msg - the message to wrap
 * @return {Promise.<EchoResult>}
 */
const promised_echo = function (msg) {
  return new Promise((resolve, reject) => {
    resolve({message: msg})
  })
}

Invoking this function will return a Promise which we expect will ultimately resolve into an EchoResult, so that by calling

promised_echo('hello from promised echo!')
  .then(res => {
    console.log(res)
  })

we can expect

{ message: 'hello from promised echo!'}

to be printed to the console.

Woo hoo! *triumphal fanfare* We've used a Promise!

Now comes the part that confuses a lot of people.

What would you expect our promised_echo function to return? Let’s find out.

const result =
  promised_echo('returns a Promise!')

console.log({result})

Yup! It returns a Promise!

{ result: Promise { <pending> } }

This is because we defined the promised_echo function above to do just that. Easy peasy.

But what would you expect promised_echo that has been chained with a .then() function to return?

const result =
  promised_echo('hello from a Promise!')
    .then(msg => {
      return msg
    })

console.log({result})

Oh! Surprise! It also returns a Promise!

{ result: Promise { <pending> } }

Fear not! If you expected it to return the string hello from a Promise!, you are not alone.

This makes perfect sense because since both the then() and catch() functions (known collectively as "thennables") are designed to be chained to Promises, and you can chain multiple thenables onto a Promise, a thenable cannot, by definition, return any useable, predictable value to its calling context!

In the OP’s example code, you have

.then((result) => {
  // ... 
  console.log(sum); // prints sum correctly: 30
  return app.ask('You have spend ' + sum); // returns 0 for sum
}

where the final thennable in your Promise chain attempts to return the result of calling your app.ask() function which, as we have seen, cannot be done.

So to be able to use a value passed to a thenable, it seems that you can only use it inside the thennable callback itself.

This is the single greatest misunderstanding in all of Promise-dom!

"But WAIT!”, as the early morning TV huckster says, “There's MORE!"

What about the other kind of thennable, the .catch() function?

Here we find another surprise!

It works exactly as we've discovered the .then() function does, in that it cannot return a value and moreover, cannot throw an error we can use to locate the problem in our code!

Let's redefine promised_echo to fail rather ungracefully:

const bad_promised_echo = function (msg) {
  return new Promise((resolve, reject) => {
    throw new Error(`FIRE! FAMINE! FLEE!! IT'S TEOTWAWKI!`)
  })
}
// TEOTWAWKI = The End Of The World As We Know It!

Such that, running

bad_promised_echo('whoops!')

will fail with:

(node:40564) UnhandledPromiseRejectionWarning: 
  Unhandled promise rejection (rejection id: 1): 
    Error: FIRE! FAMINE! FLEE!! IT'S TEOTWAWKI!

Wait, what?

Where has my stacktrace gone?

I need my stacktrace so I can determine where in my code the error occurred! ("I need my pain! It's what makes me, me!" - paraphrased from James T. Kirk.)

We lose our stacktrace because, from the perspective of the Promise's callback function, we have no runtime context from which we can derive it. (More on this below.)

Oh, I know! I'll just trap the error using the other kind of thennable, the .catch() function.

You'd think so, but no dice! because we still lack any runtime context. I'll save you the example code this time. Trust me, or better yet, try it yourself! :)

Promises run outside of the usual flow of program control, in a dim twilit world of things that run asynchronously, things that we expect will complete sometime in...

(this is where I strike my masterful scientist pose, staring off into the distance with arm upraised and finger pointing, and ominiously intone..)

THE FUTURE!

*queue swoopy scifi theremin music*.

And that's the really interesting, but confusing thing about asynchronous programming, we run functions to accomplish some goal but we have no way to know when they will finish which is the very nature of asynchronicity.

Consider this code:

request.get('http://www.example.com').then(result => {
  // do something with the result here
})

When might you expect it to complete?

Depending on several unknowable factors, such as network conditions, server load, etc., you cannot know when the get request will complete, unless you have some spooky precognitive ability.

To illustrate this, let's go back to our echo example and inject a little of that "unknowability" into our function by delaying the resolution of our our Promise by a smidge:

let result

const promised_echo_in_the_future = function (msg) {
  return new Promise(function(resolve, reject) {
    setTimeout(() => { // make sure our function completes in the future
      result = msg
      resolve({
        message: msg
      })
    }, 1) // just a smidge
  })
}

And run it (I think you know where I'm going with this by now.)

promised_echo_in_the_future('hello from the future!')

console.log({result})

Since the resolution of our Promise occurs just a smidge after our console log function attempts to reference it, we can expect

{ result: undefined }

to be printed on the console.

OK, thats a lot to digest and I commend you for your patience.

We have only one more thing to consider.

How about async/await? Can we use them to expose the resolved value of an async function so we can use it outside of that function’s context?

Unfortunately, no.

Because async/await is really just syntactic sugar used to hide the complexity of Promises by making them appear to operate synchronously,

const async_echo = async function (msg) {
  let res = await promised_echo(msg)
  return res
}

let result = async_echo();

console.log({result})

will print the expected result to the console

{ result: Promise { <pending> } }

I hope this clears things up a little.

The best advice I can give when using Promises is:

Always use any resolved result of invoking a Promise chain within the callbacks of the chain itself and handle any errors created in Promise chains the same way.



来源:https://stackoverflow.com/questions/49322403/function-returning-too-early-before-filter-and-reduce-finish

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!