When "Return" is different from "Return await" in an asynchronous function in Javascript?

Asked

Viewed 641 times

7

I was doing a review of MR and saw a test similar to this:

it('...', async () => {
  return await new Promise((resolve, reject) => {
    request(app.getHttpServer())
      .get('...')
      .send({ /* ... */ })
      .end((err, res) => {
        if (err) {
          return reject(err);
        }
        // expects ...
        resolve();
      });
  });
});

My question is whether this await makes some difference in code behavior since there is no logic after it.

Another question is whether it is necessary to mark the function as async since a Promise is already being explicitly returned. The previous code has the same behavior as the code below?

it('...', () => {
  return new Promise((resolve, reject) => {
    request(app.getHttpServer())
      .get('...')
      .send({ /* ... */ })
      .end((err, res) => {
        if (err) {
          return reject(err);
        }
        // expects ...
        resolve();
      });
  });
});

2 answers

9


Briefly, the return is different from return await when you’re inside a block try.


In the specific case of the question, there is no difference. There, return is the same as return await because you’re off a block try (which is where the difference happens).

However, there are some cases where there may be a difference. Most of the time, it is the same thing. However, it is important to know when return and return await are different since the behavior of the code can be slightly changed.

What is async?

When you mark a function as async, she at all times will return a Promise. In case an error is launched within this asynchronous function, the returned promise will be of the type rejected. Otherwise, it will be solved with some value. In the case of not using a return explicitly, a solved promise with value will be returned undefined.

Overall, it is ideal to use asynchronous functions only when you use a await explicit within them. Otherwise, the code may end up being counterintuitive for some who do not know exactly the behavior of asynchronous functions.

If you just want to return a promise (as in the question code), it is not necessary to use async or await, since there is no real need to await the completion of the promise.

The operator await

The operator await can only be used inside functions marked as asynchronous. It basically waiting by the conclusion of the promise given to him:

  • If the promise is resolved, it will "unpack" the value from within the Promise, which will be the value returned by the expression.
  • If the promise is rejected, it will throw the error, which can be captured in blocks try/catch.

From this second item of the list above, it is possible to infer the difference between the return and the return await.

Returning a promise - return <promise>

Consider the example below, where we have an asynchronous function foo calling a function waitAndMaybeReject, that returns a promise that can resolve with any value or reject with some error.

async function foo() {
  try {
//  ↓↓↓↓↓↓
    return waitAndMaybeReject();
  }
  catch (e) {
    return 'caught';
  }
}

The function waitAndMaybeReject returns a promise. However, foo does not really wait for the conclusion of this promise. The function foo simply returns (immediately) to Promise returned by the application of waitAndMaybeReject. In this case, there occurs a "delegation" of the promise.

As the promise was returned before being, in fact, resolved or rejected, one can affirm that the try/catch is useless there, because even if waitAndMaybeReject reject itself with some error, the catch will not be able to capture him, since the promise will already have been returned to those who called foo.

Returning the value of a completed promise - return await <promise>

Now consider this code (still with the same function waitAndMaybeReject of the previous example):

async function foo() {
  try {
//  ↓↓↓↓↓↓ ↓↓↓↓↓
    return await waitAndMaybeReject();
  }
  catch (e) {
    return 'caught';
  }
}

Note now that as the operator was used await, we are indeed awaiting the completion of the promise returned by waitAndMaybeReject. Thus, in the case of the promise returned by waitAndMaybeReject is rejected, the block catch will be able to capture the exception.

The promise, with the return await was not delegated by foo to its calling. On the contrary: the promise was long-awaited for foo and its value resolved (or rejected) were properly treated by foo.

Note that now foo returns the value solved by the promise that waitAndMaybeReject returns, OR "caught", in the event of rejection of the promise in question.

Completion

Generally, return await only makes a difference when the return is wrapped by some block try/catch.

If you need to treat the value that will be completed by a promise before to be actually returned, use return await. Otherwise, return (delegate the promise) it makes no difference, since, as we have seen, every asynchronous function always returns a promise.

Note that there is no difference in execution time. The difference in fact is where the promise will be completed.

Personally, I don’t like to wear return await because people don’t really understand this difference (often because they don’t even know it exists). Let’s face it, Javascript is and always has been a language with a huge number of subtle differences like this.

The following code has the same effect as return await. The difference is that, in my view, it is clearer:

function foo() {
  try {
    const val = await waitAndMaybeReject();
    return val;
  }  catch (e) {
    return 'caught';
  }
}

Is the same as:

function foo() {
  try {
    return await waitAndMaybeReject();
  }  catch (e) {
    return 'caught';
  }
}

It’s up to you to decide which one you prefer.

Like the return await only has a differential effect when inside blocks try/catch, Eslint provides a rule called no-return-await who takes care of it for you.

I don’t believe you, show me all this!

See the functional example below:

fooWithReturn(true)
  .then((resolvedValue) => console.log('1 fooWithReturn(throw).then =', resolvedValue)) // <não executará>
  .catch((errorValue) => console.log('1 fooWithReturn(throw).catch =', errorValue)); // Erro não tratado!

fooWithReturn(false)
  .then((resolvedValue) => console.log('2 fooWithReturn(dont throw).then =', resolvedValue)) // Yay!
  .catch((errorValue) => console.log('2 fooWithReturn(dont throw).catch =', errorValue)); // <não executará>
  
fooWithReturnAwait(true)
  .then((resolvedValue) => console.log('3 fooWithReturnAwait(throw).then =', resolvedValue)) // caught
  .catch((errorValue) => console.log('3 fooWithReturnAwait(throw).catch =', errorValue)); // <não executará>

fooWithReturnAwait(false)
  .then((resolvedValue) => console.log('4 fooWithReturnAwait(dont throw).then =', resolvedValue)) // Yay!
  .catch((errorValue) => console.log('4 fooWithReturnAwait(dont throw).catch =', errorValue)); // <não executará>

async function fooWithReturn(shouldThrow) {
  try {
    return waitAndMaybeReject(shouldThrow);
  } catch (e) {
    return 'caught';
  }
}

async function fooWithReturnAwait(shouldThrow) {
  try {
    return await waitAndMaybeReject(shouldThrow);
  } catch (e) {
    return 'caught';
  }
}

function waitAndMaybeReject(shouldThrow) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldThrow) {
        reject('Erro não tratado!');
      } else {
        resolve('Yay!');
      }
    }, 500);
  });
}


The examples of this response were based on the article "await vs Return vs Return await", by Jake Archibald.

2

The function shall be marked as async when there is an explicit call to a await within it. It can be freely marked as async when we want to run it asynchronously, even when it has no statement await.

As to the doubt whether the await makes a difference, the answer is: no. However, if the function only returns a promise, it makes no sense to use the await before, because it completely loses the sense. In this case, the best thing is just to return and let those who called solve the promise, making proper use of asynchronous calls.

A await after a return is counterintuitive, since the JS resolves the file to then return a new file (two files, but only a solved value in fact). Therefore, good practice is always to return direct and let who calls solve (unless you want to add a treatment, but then would not make a return directly).

  • 1

    I did some tests here, but I couldn’t verify this "waste of time" that you say exists. The code ran "at the same time" with and without the delegation. I could try to illustrate it more concretely?

  • 1

    Ah, the waste of time is very subtle. Unless it’s a really expensive call (like an HTTP call), you won’t even notice. And even if it is called HTTP, the waste of time would occur if another function could finish in the time it waited for this solution. When I say "waste of time", it’s about how the JS interpreter works. It will "waste time" solving, only then it will return. It could simply return and it would be all right. But as I said, it’s subtle and only noticeable on a large scale processing.

  • 1

    But the waiting time is associated with those who will in fact wait for the resolution of the promise. And I do not believe that the function that (in fact) makes this waiting has any difference. It makes no difference if function a wait or if function b (call for a) wait. After all, once you have the value, it is not necessary to wait for it again. As the math says, the order of the factors does not change the product. If there is actually a time difference, it will be infinitesimal, but I don’t think it will be associated with the cost of the call (as in HTTP), but rather with the fact that there is an extra function in the stack.

  • 1

    But the function a does not know that it has the value, even if b it’s already solved. It’s a new trial. That’s why bad practice. Unless the JS is optimized for this (I don’t really know), it’s two stroke. The difference is that the resolution time is super tiny, but it makes a hell of a difference when you have hundreds of thousands of calls solving something really expensive, like HTTP calls, database connections and so on.

  • 1

    So, HTTP has already been surpassed, but the time to resolve Promise, return the state and return to the stack, no. If I waste a single time every time, it accumulates. And when accumulated is costly.

  • 1

    I thought I answered who asked the question in my first kkkkkk answer but yes, you should know everything I said and perhaps question this waste of time. But it really exists. I even did refactoring work on returns by perceiving an application’s misbehavior when it was clogged with requests. Perhaps the framework used was not very well designed, but we were able to receive more requests per second.

  • 1

    A new promise will indeed be created, but at this point (without delegating the promise), the value is already known, therefore, waiting time is unique it is not possible or justifiable to wait for something that is already possessed. In addition, Javascript, being guided by asynchronous calls (increasingly tied to promises), could not allow this type of delay. That is why I insist that there is no time difference (regarding the time to wait for the conclusion of the promise).

  • 1
Show 3 more comments

Browser other questions tagged

You are not signed in. Login or sign up in order to post.