TypeScript

Fix Vite 'Top-level await is not available' build error

Why vite dev works but vite build fails on a top-level await, which setting fixes the build and which one only fixes dev, reproduced on Vite 2.9, 5.4, 7.3 and 8.3.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

vite build lowers your code to the browsers in build.target. In Vite 2 to 6 the default target is a list of browsers from before top-level await ("chrome87", "edge88", "es2019", "firefox78", "safari13.1" in Vite 2), and esbuild does not transform a top-level await for older targets, so it fails. The plugin that applies build.target only runs in vite build, which is why dev works. Set build.target: 'esnext' (or es2022), or tell esbuild the feature is available with esbuild: { supported: { 'top-level-await': true } }. If the await sits inside a dependency, optimizeDeps.esbuildOptions fixes dev only; the build still needs build.target. Vite 7 and Vite 8 build the same code with no config at all.

The question is the Stack Overflow question on the Vite top-level await build error: a TypeScript project that runs with npm run dev but fails to build because src/main.ts contains const shopsData = await fetchStarbucksShops(); at module level. Everything below was reproduced on 19 October 2026 in the official node:22-bookworm-slim Docker image (Node 22.23.3), with Vite 2.9.18, 5.4.21, 7.3.1 and 8.3.4 and TypeScript 5.9.3, on a two-file project shaped like the question's:

ts
async function fetchStarbucksShops(): Promise<{ name: string }[]> {
  return [{ name: 'London Bridge' }];
}
 
const shopsData = await fetchStarbucksShops();
document.querySelector('#app')!.textContent = `${shopsData.length} shop(s)`;

With no vite.config.js, Vite 2.9.18 prints the target list quoted in the question:

text
vite v2.9.18 building for production...
transforming...
✓ 3 modules transformed.
[vite:esbuild-transpile] Transform failed with 1 error:
assets/index.74c69252.js:48:18: ERROR: Top-level await is not available in the configured target environment ("chrome87", "edge88", "es2019", "firefox78", "safari13.1")

Vite 5.4.21 fails on the same file. Only the list changed:

text
x Build failed in 73ms
error during build:
[vite:esbuild-transpile] Transform failed with 1 error:
assets/index-!~{001}~.js:42:18: ERROR: Top-level await is not available in the configured target environment ("chrome87", "edge88", "es2020", "firefox78", "safari14" + 2 overrides)

Why the build lowers code that dev leaves alone#

The Vite 5 build options page documents the default of build.target as the special value 'modules', which Vite replaces with es2020, edge88, firefox78, chrome87 and safari14. The Vite 6 build options page gives the same list, though Vite 6 itself was not part of the reproduction. The Vite 5 page adds that the transform is performed with esbuild and that "the build will fail if the code contains features that cannot be safely transpiled by esbuild". The esbuild JavaScript content-type page puts top-level await in its table of syntax features that are "currently always passed through un-transformed", unsupported when the target is below es2022. esbuild therefore checks it against every browser in the target and stops if any of them lacks it.

According to MDN's browser compatibility data for await, top-level await arrived in Chrome 89 and Firefox 89. Safari has it from version 15 as a partial implementation (WebKit bug 242740: several modules importing the same top-level-await module at once), and the data lists full support only from Safari 27. Every browser in the old default lists is older than those first versions, so the error is expected. Your code is not wrong.

The versions line up like this. Vite 2.9.18 and Vite 5.4.21 were reproduced. The Vite 4 and Vite 6 docs give the same es2020 list as Vite 5. The Vite 3 docs describe the 'modules' value without printing a browser list, but the Vite 3 constants file sets it to es2020, edge88, firefox78, chrome87 and safari13. Vite 3 itself was not run.

The error label in the log names the step. In the Vite 2 esbuild plugin source, vite:esbuild-transpile is created by the build-only buildEsbuildPlugin, runs on each rendered chunk and passes config.build.target to esbuild. The dev server never runs that plugin, which fits the asker's report that npm run dev worked. The dependency pre-bundler is a different matter, covered below.

Fix: raise the target, or declare the one feature#

Pick one of these. Options 1 and 2 were each a single change that fixed the build on Vite 2.9.18. Option 3 was tested on Vite 7.3.1 and 8.3.4.

  1. Target modern browsers in general. This is the second answer under the question, and the one most people need:
js
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
  build: { target: 'esnext' },
})

esnext stops almost all syntax lowering. If you still want older features down-levelled, name the browsers you support instead. A list such as ['chrome89', 'edge89', 'firefox89', 'safari15'] also built in the reproduction. On Vite 5.4.21, build: { target: 'es2022' } built too:

text
vite v5.4.21 building for production...
transforming...
✓ 3 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html                0.14 kB │ gzip: 0.13 kB
dist/assets/index-Eo1m8ckS.js  0.84 kB │ gzip: 0.47 kB
✓ built in 74ms
  1. Keep the target and tell esbuild only that top-level await is safe. The second answer offers this as an alternative, and the third answer calls it its preferred method because it ignores one feature only:
js
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
  esbuild: { supported: { 'top-level-await': true } },
})

The esbuild supported setting overrides the target feature by feature, and top-level-await is one of the feature names it lists. With this config, Vite 2.9.18 built the project while build.target kept its default list.

  1. Upgrade Vite. The Vite 7 build options page sets the default to 'baseline-widely-available', which is chrome107, edge107, firefox104 and safari16. The current docs for Vite 8 give chrome111, edge111, firefox114, safari16.4 and ios16.4. Each of those versions is at or above the first version MDN gives for top-level await, with Safari still under its partial-implementation note. In the reproduction, Vite 7.3.1 and Vite 8.3.4 built the unchanged project with no vite.config.js. Upgrading major versions has its own migration work, so treat this as the long-term fix, not the quick one.

Verify the fix#

Run the production build again and check the exit code, not just the console:

bash
npx vite build && echo "build ok"

With build.target: 'esnext', Vite 2.9.18 printed:

text
vite v2.9.18 building for production...
transforming...
✓ 3 modules transformed.
rendering chunks...
dist/index.html                 0.14 KiB
dist/assets/index.74c69252.js   0.83 KiB / gzip: 0.47 KiB

A green build only means the transform step accepted the code: esnext stops the lowering, it does not polyfill anything. Serve dist with npx vite preview and open it in the oldest browser you support before you ship.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

When the await is inside a dependency#

The accepted answer sets two options at once, optimizeDeps.esbuildOptions.target and build.target, for people who see the error in both dev and build. The reproduction shows why one of them is not enough. A local package tla-dep whose index.js starts with const config = await Promise.resolve({ region: 'eu-west-2' }); was imported from main.ts and listed in optimizeDeps.include. The dev pre-bundler, run with npx vite optimize --force on Vite 2.9.18, failed with the same message:

text
Optimizing dependencies:
  tla-dep
✘ [ERROR] Top-level await is not available in the configured target environment ("chrome87", "edge88", "es2019", "firefox78", "safari13.1")

Adding optimizeDeps: { esbuildOptions: { target: 'esnext' } } made the pre-bundle succeed. But vite build of the same project, with that config and no build.target, still failed:

text
[vite:esbuild-transpile] Transform failed with 1 error:
assets/index.db24cd9b.js:45:15: ERROR: Top-level await is not available in the configured target environment ("chrome87", "edge88", "es2019", "firefox78", "safari13.1")

The production build bundles the dependency into your chunk, then lowers the chunk to build.target. The pre-bundler's settings never reach that step. Set optimizeDeps.esbuildOptions.target for dev and build.target for the build, as the accepted answer does. The third answer shows the equivalent with esbuildOptions: { supported: { "top-level-await": true } } scoped through optimizeDeps.include.

The same check also clears up the opposite case. With your own main.ts using top-level await, setting only optimizeDeps.esbuildOptions.target changed nothing for vite build, which failed with the original target list.

The third answer also mentions vite-plugin-top-level-await for browsers that truly lack the feature. Per that answer, the plugin transforms the await away and so changes your production code. It was not tested here.

Vite 8 with an old target: a warning instead of an error#

Vite 8 transforms with Oxc instead of esbuild, and the Vite 8 build options page says the build "will output a warning if the code contains features that cannot be safely transpiled by Oxc". With build: { target: 'es2020' }, Vite 8.3.4 exited with code 0 and printed:

text
[TOLERATED_TRANSFORM] Top-level await is not available in the configured target environment.

The warning points at src/main.ts:4:19, and the emitted dist/assets/index-CJtY4lbF.js still contains the await call. So on Vite 8 a green build does not mean your declared target is met. If you lowered the target on purpose for old browsers, read that warning as the old error. In the same version, the esbuild option and optimizeDeps.esbuildOptions are marked deprecated and converted to oxc and optimizeDeps.rolldownOptions (see the Vite shared options), so copy the Vite 2 to 6 snippets above only onto those versions.

The type-check step: TS1378 and TS1375#

Projects built with tsc && vite build can fail before Vite starts, because TypeScript has its own rule for top-level await. On the same main.ts with TypeScript 5.9.3, a tsconfig with "module": "ES2015" and "target": "ES2017" gave:

text
src/main.ts(5,19): error TS1375: 'await' expressions are only allowed at the top level of a file when that file is a module, but this file has no imports or exports. Consider adding an empty 'export {}' to make this file a module.
src/main.ts(5,19): error TS1378: Top-level 'await' expressions are only allowed when the 'module' option is set to 'es2022', 'esnext', 'system', 'node16', 'node18', 'node20', 'nodenext', or 'preserve', and the 'target' option is set to 'es2017' or higher.

Switching to "module": "ESNext" with "target": "ES2020" cleared TS1378, but TS1375 remained, because the file has no import or export:

text
src/main.ts(5,19): error TS1375: 'await' expressions are only allowed at the top level of a file when that file is a module, but this file has no imports or exports. Consider adding an empty 'export {}' to make this file a module.

Adding the moduleDetection option set to force made tsc --noEmit exit with code 0. An export {} line in the file would also make it a module, as the message says.

Neighbouring Vite build failures#

Other errors that only show up under vite build come from different stages. Vite 2's define can break a dependency's syntax, which gives Unexpected token in node_modules. A package.json pointing at a missing file gives commonjs--resolver Failed to resolve entry. A library that expects Node's global in the browser gives global is not defined.

The top-level await error is the simplest of these. Nothing is broken. Your code uses a feature that the configured browsers are too old for, and the fix is to state which browsers you actually ship to. On Vite 2 to 6 that means build.target for the build, plus optimizeDeps.esbuildOptions when a dependency carries the await. On Vite 7 and 8, the default target already allows it, and a deliberate old target on Vite 8 gives a warning to read, not an error.

Further reading: Expected identifier but found "import" (define name) in Vite.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote