Why are await and async valid variable names?
Asked Answered
P

1

53

I was experimenting with how / is interpreted when around different keywords and operators, and found that the following syntax is perfectly legal:

// awaiting something that isn't a Promise is fine, it's just strange to do:
const foo = await /barbaz/
myFn()

Error:

Uncaught ReferenceError: await is not defined

It looks like it tries to parse the await as a variable name..? I was expecting

await is only valid in async function

or maybe something like

Unexpected token await

To my horror, you can even assign things to it:

const await = 'Wait, this actually works?';
console.log(await);

Shouldn't something so obviously wrong cause a syntax error, as it does with let, finally, break, etc? Why is this allowed, and what the heck is going on in the first snippet?

Procora answered 1/5, 2019 at 9:56 Comment(0)
P
87

Reserved keywords cannot be used as identifiers (variable names). Unlike most other special Javascript words (like those listed in the question, let, finally, ...), await is not a reserved keyword, so using it as a variable name does not throw a SyntaxError. Why wasn't it made into a reserved keyword when the new syntax came out?

Backwards compatibility

Back in 2011, when ES5 was still a relatively new thing, code that used await (and async) as variable names was perfectly valid, so you may have seen something like this on a couple sites:

function timeout(ms) {
  var await = $.Deferred();
  setTimeout(await.resolve, ms);
  return await.promise();
};

The choice of that variable name may seem odd, but there was nothing wrong with it. await and async have never been reserved keywords - if the writers of the ES2017 specification made await into a reserved keyword, and browsers implemented that change, people visiting those older sites on newer browsers would not be able to use those sites; they would likely be broken.

So perhaps if they were made into reserved keywords, a few sites which chose a peculiar variable name wouldn't work properly - why should the existence of those sites permanently affect the future evolution of ECMAscript and result in confusing code like in the question?

Because browsers will refuse to implement a feature which breaks existing sites. If a user finds that a site does not work on one browser, but works on another, that will incentivize them to switch browsers - the maker of the first browser would not want that, because that would mean less market share for them, even if it's a feature which makes the language more consistent and understandable. In addition, the editors of the specification do not want to add something that will never be implemented (or will only be implemented sporadically), because then the specification would lose some of its status as a standard - contrary to its main goal.

You could see these interactions in action with Array.prototype.flatten and Array.prototype.contains - when browsers started shipping them, it was found that they broke a few existing sites due to name conflicts, so the browsers backed out of the implementation, and the specification had to be tweaked (the methods were renamed to .flat and .includes).


There actually is a situation in which await cannot be used as an identifier, which is inside of ES6 modules:

<script type="module">
  const await = 'Does it work?';
</script>

This is because while ES6 (ES2015) modules were being figured out, async/await was already on the horizon (initial commit for the async/await proposal can be seen at the beginning of 2014), so while designing modules, await could be made a reserved keyword in preparation for the future, without breaking any existing sites.


With regards to the first snippet in the question:

const foo = await /barbaz/
myFn()

This is syntactically valid because await is a valid variable name outside of async functions, and the interpreter thinks you're trying to divide, rather than use a regular expression:

const foo = await / barbaz / myFn()

Not relying on Automatic Semicolon Insertion would have identified the problem earlier, because the last / could not have been interpreted as division:

const foo = await /barbaz/;
myFn();

This exact somewhat-ambiguous situation was actually specifically brought up in a TC39 meeting on async/await:

YK: What are you worried about?

WH: Ambiguities on code sequences that start with await/ and then get interpreted in diverging ways (due to the await-as-identifier vs await-as-operator distinction that flips the / between division and starting a regexp) by cover grammars vs. real grammars. It's a potential bug farm.

Procora answered 1/5, 2019 at 9:56 Comment(2)
Talking to yourself today.. No, only joking good post. Before your response my initial gut reaction was compatibility. I assume it's also why of, as in for of is not reserved, although even in module mode this is still valid.Snapper
Your assumption that backwards compatibility is in play is correct. It is the reason why we have to also add the async keyword at the function declaration site to change the context from "await is an identifier" to "await is a keyword". For the exact same reason, we have function* and yield, it is, too, a valid identifier name outside of generators.Respite

© 2022 - 2024 — McMap. All rights reserved.