Top-level await allows modules to act as asynchronous functions, meaning they can pause module execution to await promises. This is extremely useful for conditionally loading async dependencies—like polyfills or heavy secondary libraries—only when required by the browser.
By utilizing top-level await, you can encapsulate the conditional loading logic inside a single module, effectively preventing downstream consumer modules from executing until the dependency is fully loaded and ready.
Conditional polyfill pattern
While top-level await can be used to conditionally load any async dependency that's a module, a good use of the conditional depenency loading pattern is to conditionally load polyfills for browsers that don't support a specific feature. This approach encapsulates feature detection and the dynamic import inside a single dependency module.
In the following case, the popover attribute polyfill is conditionally loaded if it isn't present on HTMLElement.prototype:
// conditionally-load-polyfill.js
// Check if the feature is missing before doing work.
// MANDATORY: Prefer checking HTMLElement.prototype over window or document
// when checking for a global DOM attribute or property like popover.
if (!('popover' in HTMLElement.prototype)) {
// Use top-level await to pause the execution of any module that imports this file
// until the polyfill finishes downloading and executing.
await import('/path/to/popover-polyfill.js');
}
// Export a marker if needed by your application
export const polyfillLoaded = true;
// main.js
// MANDATORY: Because conditionally-load-polyfill.js uses top-level await,
// this import will block execution of main.js until the polyfill is ready.
import './conditionally-load-polyfill.js';
// Now it is safe to use the feature (e.g., showing a popover)
const myPopover = document.getElementById('my-popover');
if (myPopover) {
myPopover.showPopover();
}
Sequencing module imports and avoiding the pre-Safari 27 bug
MANDATORY: You must structure your module graph so the top-level await module is imported from a single entry point and completes before downstream modules evaluate:
- DO NOT place sibling static imports in an entry file (for example,
import './conditionally-load-polyfill.js'; import './app.js';). Per the ECMAScript module evaluation specification, top-levelawaitonly suspends the parent module that imports it—not sibling static imports—so./app.jswill evaluate concurrently in all browsers before the polyfill finishes loading. - DO NOT import the top-level
awaitmodule from multiple sibling modules simultaneously (for example, having botha.jsandb.jsstatically import./conditionally-load-polyfill.js). While Safari 27 fixed WebKit's module loader to support concurrent imports of top-levelawaitmodules per spec, Safari 15–26.6 fails when multiple modules simultaneously import a module containing a top-levelawait(see MDN browser compatibility).
Instead, choose one of two patterns depending on your module structure:
- Single entry module (shown in
main.jsabove): If your entry module runs the dependent initialization logic directly, statically import./conditionally-load-polyfill.jsat the top of that module and run your code in its body. - Bootstrapping separate application modules: If your entry module delegates to downstream application modules (such as
./app.js), chain dynamicimport()calls with top-levelawaitso./app.jsis only fetched and evaluated after the polyfill module resolves:
// main.js
// MANDATORY: Chain dynamic imports with top-level await when bootstrapping separate
// application modules so './app.js' waits for the polyfill to finish loading.
// DO NOT use sibling static imports (`import './conditionally-load-polyfill.js'; import './app.js';`),
// which evaluate concurrently and execute './app.js' before the polyfill is ready.
await import('./conditionally-load-polyfill.js');
await import('./app.js');
Fallback strategies
While basic top-level await has been supported across major browsers since 2021 (Chrome 89, Edge 89, Firefox 89, and Safari 15), it only reached full spec compliance in Safari 27 (see MDN browser compatibility).
- Safari 15–26.6: As long as you follow the single-entry-point pattern in the previous section so multiple sibling modules do not concurrently import the top-level
awaitmodule, you can safely rely on top-levelawaitwithout an additional fallback. - Legacy browsers (pre-2021): If your application must support browsers without any top-level
awaitsupport, avoid top-levelawaitand wrap your conditionalimport()orchestration in a standardasyncfunction or.then()promise chain.