How to Fix Webpack 5 'experiments' Not Enabled for topLevelAwait in a React App
Threat/Impact Level: HIGH | Downtime Risk: HIGH (build fails completely, zero output) | Time to Fix: 5 mins
TL;DR
- What broke: A module in your React app uses
awaitat the top level of a file. Webpack 5 treats this as a hard error unlessexperiments.topLevelAwait: trueis explicitly set — your build produces zero output. - How to fix it: Add
experiments: { topLevelAwait: true }to yourwebpack.config.js(or the appropriate override for CRA/CRACO/Next.js). - Fast path: Use our Client-Side Sandbox above to drop your
webpack.config.jsand auto-refactor it — secrets are redacted locally, nothing leaves your browser.
The Incident (What Does the Error Mean?)
You hit this during webpack build or webpack serve:
ERROR in ./src/api/client.js
Module parse failed: Cannot use keyword 'await' outside an async function (3:15)
File was processed with these loaders: ...
You may need an additional loader to handle the result of these loaders.
> await fetch('/api/bootstrap');
or the more explicit webpack 5 variant:
ERROR in ./src/api/client.js 3:0
Module build failed (from ./node_modules/babel-loader/lib/index.js):
SyntaxError: /app/src/api/client.js: Modules using top-level await must set
'experiments.topLevelAwait: true' in your webpack configuration.
Immediate consequence: Webpack halts the entire compilation. No JS bundle is emitted. Your dev server refuses to start or your CI pipeline exits non-zero. This is not a runtime warning — it is a hard build-time abort.
The Attack Vector / Blast Radius
This is a build-system misconfiguration, not a runtime security hole, but the blast radius in a CI/CD pipeline is severe:
- Full deployment blockage. Every downstream stage — Docker image build, S3 sync, Kubernetes rollout — never executes. A single developer merging a file with top-level await silently kills the pipeline for the entire team.
- Babel/SWC mismatch amplification. If you have Babel configured to strip
async/awaitvia@babel/plugin-transform-async-to-generatorbut webpack's module system still sees the raw ESM syntax before Babel processes it, you get a confusing double-failure that looks like a loader ordering bug. It isn't — it's this flag. - Cascading monorepo failures. In an Nx or Turborepo monorepo, one shared library using top-level await will break every app that imports it, even apps that don't directly use
awaitat the top level themselves. - Silent CRA failure mode. Create React App wraps webpack and swallows the raw error in some versions, surfacing only
Failed to compilewith no actionable detail — developers waste hours blaming Babel configs.
How to Fix It
Basic Fix — Ejected or Custom webpack.config.js
// webpack.config.js
module.exports = {
mode: 'production',
entry: './src/index.js',
+ experiments: {
+ topLevelAwait: true,
+ },
module: {
rules: [
{
test: /\.[jt]sx?$/,
use: 'babel-loader',
exclude: /node_modules/,
},
],
},
};
CRACO Override (Create React App — non-ejected)
// craco.config.js
module.exports = {
webpack: {
- configure: (webpackConfig) => {
- return webpackConfig;
- },
+ configure: (webpackConfig) => {
+ webpackConfig.experiments = {
+ ...webpackConfig.experiments,
+ topLevelAwait: true,
+ };
+ return webpackConfig;
+ },
},
};
Next.js Override
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
+ webpack: (config) => {
+ config.experiments = {
+ ...config.experiments,
+ topLevelAwait: true,
+ };
+ return config;
+ },
};
module.exports = nextConfig;
Enterprise Best Practice — Pair with Correct Babel Target
Enabling the flag alone is insufficient if your Babel config targets environments that don't support top-level await (ES2017 and below). Mismatched targets cause webpack to emit code that crashes in older Node.js or browser runtimes even though the build succeeds.
// babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
- targets: '> 0.25%, not dead',
+ targets: { node: '14.8', browsers: 'last 2 Chrome versions, last 2 Firefox versions' },
// Node 14.8+ and modern browsers natively support top-level await
// Do NOT use useBuiltIns: 'usage' with top-level await modules — it breaks async chunk splitting
},
],
'@babel/preset-react',
'@babel/preset-typescript',
],
};
Critical: If you must support environments without native top-level await, do not use top-level await. Refactor the module to use an async IIFE or a module-level init function called from your app entry point. The experiments flag does not polyfill — it only unlocks webpack's internal module graph handling for the syntax.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. ESLint Rule — Catch It Before Webpack Sees It
Install eslint-plugin-unicorn and enable the rule that flags top-level await in projects where the webpack config hasn't been updated:
// .eslintrc.js
module.exports = {
plugins: ['unicorn'],
rules: {
+ 'unicorn/prefer-top-level-await': 'warn',
+ // Flip to 'error' and add a pre-commit hook to block the commit
+ // until the webpack experiments flag is confirmed present
},
};
2. Custom Webpack Validate Plugin (Fail Fast in CI)
Add a validation step to your webpack config that asserts the flag is set before any compilation begins:
// scripts/validate-webpack-experiments.js
const config = require('../webpack.config.js');
if (!config.experiments?.topLevelAwait) {
console.error('[CI GATE] webpack.config.js is missing experiments.topLevelAwait: true');
process.exit(1);
}
console.log('[CI GATE] webpack experiments: OK');
Add to package.json:
"scripts": {
+ "prestart": "node scripts/validate-webpack-experiments.js",
+ "prebuild": "node scripts/validate-webpack-experiments.js",
"start": "webpack serve",
"build": "webpack build"
}
3. Renovate / Dependabot Config Lock
When webpack major versions bump, experiments API surface changes. Pin a post-upgrade test:
# .github/workflows/ci.yml
- name: Validate webpack experiments config
run: node scripts/validate-webpack-experiments.js
- name: Build
run: npm run build
4. Checkov / Semgrep Custom Rule
For teams using Semgrep in their pipeline, write a rule that detects top-level await in .js/.ts files and checks for the corresponding webpack flag:
# semgrep-rules/no-tla-without-webpack-flag.yaml
rules:
- id: top-level-await-without-webpack-experiment
patterns:
- pattern: |
await $EXPR;
message: |
Top-level await detected. Verify that experiments.topLevelAwait: true
is set in webpack.config.js before merging.
languages: [js, ts]
severity: WARNING
paths:
exclude:
- node_modules
- '**/*.test.*'