In JavaScript, can you use `await` outside an async function? Say where that is legal and where it is still a SyntaxError.
answer
- depends on the parse goal
- ES2022 addition, not universal
- fine in .mjs, never in .cjs
- classic script tag fails to parse
- dynamic import() is the escape hatch
basics
~20 sYes, but only at the top level of an ES module. Top-level await is an ES2022 feature; in a classic script, in a CommonJS file, or inside any non-async function, await outside an async function is a SyntaxError.
solid answer
~40 sSince ES2022 you can write `await` at the top level of an **ES module** — a `<script type="module">`, a `.mjs` file, or a `.js` file in a package whose `package.json` says `"type": "module"`. Everywhere else it is a parse-time SyntaxError: a classic `<script>` tag, a CommonJS file loaded by `require()`, and the body of any function that is not marked `async`. The reason is that only module code can evaluate asynchronously — a CommonJS `require()` call must return `module.exports` synchronously, so there is nowhere for the wait to happen. In CommonJS the usual substitutes are an `async` function you call immediately, or dynamic `import()`, which returns a promise and works in both module systems.
code
javascript · 4 lines// config.mjs — parsed with the module goal, so this is legal
const res = await fetch('https://example.com/config.json');
export const config = await res.json();
export const loadedAt = Date.now();go deeper
Recall the one place it is allowed — the top level of an ES module — and be able to name what makes a file a module: type="module", a .mjs extension, or "type": "module" in package.json.
Explain why the restriction exists: require() must return module.exports synchronously, so a CommonJS module has nowhere to suspend, while ES module evaluation is already asynchronous. Note that it is a parse-time error, not a runtime one.
Be ready to say what you actually do in a mixed codebase: dynamic import() from CommonJS, an exported async initialiser instead of an exported value, and the file-extension rules that decide the goal when a package is being migrated.
Own the migration tradeoff — making a package ESM-only unlocks top-level await but cuts off synchronous require() consumers. Be able to argue for dual entry points, an ESM-only cutover, or keeping async setup out of module scope entirely.
## Two parse goals JavaScript source text is parsed under one of two goals defined by the specification: **Script** or **Module**. This is not a property of the code you wrote — it is decided by how the host hands the file to the engine. Code is parsed as a **Module** when it is loaded as an ES module: `<script type="module">` in a browser, a file reached through an `import` statement, a `.mjs` file, or a `.js` file inside a package whose `package.json` contains `"type": "module"`. Code is parsed as a **Script** when it is a classic `<script>` tag, a string passed to `eval`, or a CommonJS file that `require()` loads. ## What top-level await added Before ES2022, `await` was legal only inside the body of an `async` function. The top-level await proposal (shipped in ES2022, unflagged in Node 14.8+ and in all current browsers) extended that to one more place: the top level of a Module. ```js // config.mjs — legal, module goal const res = await fetch('https://example.com/config.json'); export const config = await res.json(); ``` Note what did *not* change. `await` inside a plain (non-async) function nested in a module is still a SyntaxError — the new rule is about the module's own top level, not about anything lexically inside it: ```js // still a SyntaxError, even in a module function load() { return await fetch('/x'); } ``` ## Where it is still a SyntaxError Under the Script goal, `await` outside an async function is an **early error** — the file fails to parse, so nothing in it runs at all, not even the lines above the `await`. V8 reports it as "await is only valid in async functions and the top level bodies of modules". A related detail that surprises people: in module code `await` is a fully reserved word, so `var await = 1` is a SyntaxError there, while in sloppy-mode script code that same line is legal. ## Why CommonJS cannot have it This is a design consequence, not an oversight. `require('./x')` is a synchronous function call that must return `module.exports` before the next line of the caller runs. If a CommonJS module could pause halfway through, `require()` would have to either block the thread or return something incomplete. ES modules do not have that constraint: the module system already loads and links the graph asynchronously, so it can also *evaluate* asynchronously. ## The escape hatches If you are stuck in CommonJS and need asynchronous setup, the options are: 1. **Dynamic `import()`** — an expression, not a statement, that returns a promise for the module namespace. It works in both CommonJS and ESM, and it is how a CommonJS file loads an ES module at all. ```js // loader.cjs async function main() { const { config } = await import('./config.mjs'); console.log(config); } main(); ``` 2. **An immediately-called async function** — the old `(async () => { ... })()` wrapper. It works, but nothing outside the wrapper can await its completion, so anything the wrapper produces is not available to the rest of the file synchronously. 3. **Export a promise or an async initialiser** — `module.exports = init()` or `module.exports = { getConfig }` where `getConfig` is async. Consumers then await at the point of use. Also worth knowing: in recent Node versions `require()` can load an ES module, but that only works when the module graph is fully synchronous. A module that uses top-level await cannot be `require()`d — Node rejects it rather than silently returning something half-built. ## Interview framing The question is really checking whether you understand that a module can be *asynchronous* and a script cannot. Candidates who answer "yes, since ES2022 you can await anywhere" have learned the headline and missed the constraint; candidates who answer "no, await always needs an async function" are working from a pre-2022 mental model. The precise answer names the module goal.
- Inside a module that already uses top-level await, why is `await` still illegal in a nested non-async function?Because the permission applies to the module's own top-level body, not to everything lexically inside it. A non-async function has no mechanism to suspend and resume, so `await` in its body is an early SyntaxError regardless of the surrounding goal. Mark the function `async` — and remember the caller then gets a promise back rather than the value.
- How would you make an existing CommonJS module expose a value that can only be produced asynchronously?Stop exporting the value and export the way to get it: either `module.exports = init()` so consumers await one shared promise, or export an async `getX()` that memoises its own promise on first call. Both keep `require()` synchronous while pushing the wait to the point of use.
- Does adding `"type": "module"` to package.json change how existing .js files in that package parse?Yes — every `.js` file in that package is then parsed with the module goal, which turns on strict mode, changes top-level `this` to `undefined`, removes `require`/`__dirname`, and makes top-level `await` legal. Files that need the old behaviour must be renamed `.cjs`.
saying these in an interview costs you the question
- Says await works anywhere in modern JavaScript
- Thinks top-level await is legal in a CommonJS file
- Claims a classic <script> tag supports top-level await
- Believes await is allowed in any nested function inside a module
- Says require() can load a module that uses top-level await