2
{
  async function f() {
    return 1;
  }
}

(async () => {
  await f();
})();

Note that f is in separate block.

It was okay when f wasn't async function. But with async function, I am getting error UnhandledPromiseRejectionWarning: ReferenceError: f is not defined

Why is it different for async function?

  • regular functions are `[object Function]` - `async` functions are `[object AsyncFunction]` - so, they are different from the start - MDN documentation does not have anything regarding AsyncFunction scope though – Jaromanda X May 09 '19 at 23:19
  • 1
    This is really interesting and I've spent quite some time trying to work it out. It seems that `async` functions are hoisted to the top of their block, whereas normal functions are hoisted to the nearest function scope. I have been thoroughly unable to find any documentation to explain this. It *feels* wrong to me! – lonesomeday May 10 '19 at 10:46
  • Additional investigation reveals that triggering strict mode makes the two behave equivalently. In "normal mode", the async function is limited to the block, but the normal function is hoisted to the top level. So the solution, as normal, is **always use strict mode**, but I'm still unsure about *why*. – lonesomeday May 11 '19 at 17:06

1 Answers1

1

OK, I'm there: I have worked it out.

First of all, there is a difference between strict mode (see the MDN documentation). In strict mode, the two behave equivalently:

"use strict";

{
  function f() {
    return 1;
  }
}

(async () => {
  console.log(await f());
})();

"use strict";

{
  async function f() {
    return 1;
  }
}

(async () => {
  console.log(await f());
})();

Functions in blocks were actually entirely prohibited in strict mode until ES2015, but now behave in the expected way.

In sloppy mode, functions in blocks are technically not allowed by the spec, but were actually allowed by all implementations (MDN vaguely explains this). The function, confusingly, gets hoisted to the nearest function scope. This is not intuitive behaviour, which is why it is changed in strict mode.

To make things even more confusing, an asynchronous function in sloppy mode is scoped to the block, even though a normal function would be scoped to the function. I have not found anything that documents this behaviour.

{
  console.log(g()); // works
  console.log(f()); // works
  async function f() {
    return 1;
  }
  function g() {
    return 2;
  }
}

(async () => {
  console.log(await g()); // works
  console.log(await f()); //errors
})();

So the situation is confusing in sloppy mode, because of decisions made to ensure compatibility with old code. This is, of course, a constant refrain when developing in sloppy mode, which is why you should always use strict mode.

Thanks for a really interesting question!

lonesomeday
  • 233,373
  • 50
  • 316
  • 318
  • See also [What are the precise semantics of block-level functions in ES6?](https://stackoverflow.com/a/31461615/1048572) for a hopefully less vague explanation. And yes, this only applies to normal `function` declarations, not to async functions or generator functions since there's no need for backwards-compatibility on those. – Bergi May 11 '19 at 18:15
  • @Bergi Thank you: that is helpful. It's a kind of catch-22 with backwards compatibility here, I think: both options are confusing! – lonesomeday May 11 '19 at 18:52
  • Thanks for the answer! – SungMin Hong May 17 '19 at 18:03