5

We know that async functions return a Promise implicitly. But I have a purely pedantic question. Should I put an async keyword if I return a Promise explicitly?

Is this:

const wait = async ms => new Promise(
    resolve => setTimeout(resolve, ms)
);

Any different than this?

const wait = ms => new Promise(
    resolve => setTimeout(resolve, ms)
);

I believe that technically they are identical. Is there any style guide or official recommendation behind any of these two ways to define this kind of function?

Robo Robok
  • 21,132
  • 17
  • 68
  • 126
  • 4
    I wouldn't, you're not `await`ing inside, so it's just syntax noise IMO – CertainPerformance Oct 22 '21 at 21:44
  • @CertainPerformance part of me agrees with your point, I feel like `async` keyword in JavaScript is a little confusing. I don't like it how it's required only to use with `await`. Is it like that in other languages too? – Robo Robok Oct 22 '21 at 21:58

2 Answers2

11

There are four main reasons I think about for using async functions:

  1. You want to use await.
  2. You want it to automatically catch synchronous exceptions and turn them into a rejected promise.
  3. You want it to always return a promise, regardless of what your function actually returns.
  4. You like the fact that making the function async makes it clear to callers looking at the code that the function always returns a promise - essentially self documenting.

So, if you aren't using await and you don't need point #2 and you are manually returning a promise already, then there's really no requirement to declaring the function as async.


A few more thoughts on the points above.

Point #1 requires async if you're going to use await. There is no other way around it.

Points #2 and #3 are really just programming conveniences. If you either catch your own synchronous exceptions or are sure there are no synchronous exceptions and you are controlling all code paths to return a promise, then async is not necessary.

Both points #2 and #3 can arise if your code has both a synchronous code path and an asynchronous code path, such as checking a cache and returning a value if its present in the cache and, if not in the cache, then make a network request to fetch the value. As described above, this can be coded manually without async, but the code can sometimes be a little simpler with async because it will automaticlly catch your synchronous exceptions and automatically wrap a return value in a promise.

Point #4 is just a coding style preference. If you like the "self-documenting" aspects of making the function async, you can do that as an indication that it always returns a promise.


And, for anyone interested in lots of technical detail about how async functions work internlly and have been optimized over the years, this is a fairly indepth article on the topic: V8 blog on fast async.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • How about adding `async` just for the sake of readability as "it makes sense to `await` for this function"? – Robo Robok Oct 22 '21 at 22:06
  • @RoboRobok - I suppose. Certainly that tells the caller that it will always return a promise, but generally you need some doc somewhere that tells you what the function returns anyway so I personally wouldn't do it only for that reason. But, that's certainly just a personal preference thing. – jfriend00 Oct 22 '21 at 22:09
  • @RoboRobok - I decided to add point #4 about the self-documenting aspect of making it `async`. – jfriend00 Oct 23 '21 at 01:14
  • Awesome, thanks! Also please take a look at 3limin4t0r's answer below, especially at the bottom. Not sure about you, but I wasn't aware of that difference. – Robo Robok Oct 23 '21 at 01:16
  • @RoboRobok - I've never had an issue where I'm comparing to a specific promise reference such that the `async` wrapping would be an issue. I don't even know how an outside caller would have a promise reference that the function was returning unless it was somehow passed into the function as an argument. Anyway, that is technically a difference, it just doesn't seem like something that would come up often or perhaps ever. – jfriend00 Oct 23 '21 at 01:33
  • Well, in some complex scenarios you'd want a Promise to be passed around. Also I'm a bit surprised that `async` actually wraps a Promise in another Promise. – Robo Robok Oct 23 '21 at 01:41
  • @RoboRobok - I guess I've never seen a situation where you're passing in a promise and requiring it to return the same promise (so chaining onto that promise and returning the new promise is not allowed). But, certainly if that was the case, `async` should not be used. – jfriend00 Oct 23 '21 at 01:56
  • @RoboRobok - If you want to see some of the internals of how an `async` function works with `await`, this is a very interesting [article](https://v8.dev/blog/fast-async#fn2). It does not directly address why an `async` function always rewraps a returned promise in a new promise, but I think it shows in the description of how an `async` function internally always starts with `createPromise()`. Anyway, it's interesting reading for other reasons. – jfriend00 Oct 23 '21 at 06:22
1

In the given example the async keyword is essentially only wrapping the return value in Promise.resolve(). See the async function documentation.

So on one hand you have:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

While on the other you have:

const wait = async ms => new Promise(resolve => setTimeout(resolve, ms));
// is similar to
const wait = ms => Promise.resolve(new Promise(resolve => setTimeout(resolve, ms)));

They are essentially the same and I would personally go for the variant without the async keyword.

Note:

Even though the return value of an async function behaves as if it's wrapped in a Promise.resolve, they are not equivalent.

An async function will return a different reference, whereas Promise.resolve returns the same reference if the given value is a promise.

It can be a problem when you want to check the equality of a promise and a return value of an async function.

const p = new Promise((res, rej) => {
  res(1);
})

async function asyncReturn() {
  return p;
}

function basicReturn() {
  return Promise.resolve(p);
}

console.log(p === basicReturn()); // true
console.log(p === asyncReturn()); // false
3limin4t0r
  • 19,353
  • 2
  • 31
  • 52