Initializing Enclave...

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 await at the top level of a file. Webpack 5 treats this as a hard error unless experiments.topLevelAwait: true is explicitly set — your build produces zero output.
  • How to fix it: Add experiments: { topLevelAwait: true } to your webpack.config.js (or the appropriate override for CRA/CRACO/Next.js).
  • Fast path: Use our Client-Side Sandbox above to drop your webpack.config.js and 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:

  1. 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.
  2. Babel/SWC mismatch amplification. If you have Babel configured to strip async/await via @babel/plugin-transform-async-to-generator but 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.
  3. 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 await at the top level themselves.
  4. Silent CRA failure mode. Create React App wraps webpack and swallows the raw error in some versions, surfacing only Failed to compile with 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.*'

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →