skip to content

In JavaScript, can you use `await` outside an async function? Say where that is legal and where it is still a SyntaxError.

level: juniorimportance: should knowfreq 45%

answer

  1. depends on the parse goal
  2. ES2022 addition, not universal
  3. fine in .mjs, never in .cjs
  4. classic script tag fails to parse
  5. dynamic import() is the escape hatch

basics

~20 s

Yes, 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 s

Since 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
javascript
// 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context