What is top-level await in JavaScript modules?
Assesses fundamental understanding of JavaScript conventions, runtime behavior, and memory/performance considerations.
Hiring managers look for precision, avoidance of ambiguous jargon, and ability to explain trade-offs under real production conditions.
Top-level await is a feature (introduced in ES2022) that allows you to use the await keyword at the top level of ES modules, outside of async functions. This enables modules to act as asynchronous functions themselves.
Key Characteristics:
- Module-Only: Only works in ES modules (not in scripts or CommonJS)
- Blocks Execution: The module graph execution pauses until the promise resolves
- No Async Wrapper: No need to wrap await in an async function
- Import Dependency: Modules that import a module using top-level await will wait for it
Before Top-Level Await:
// ❌ Old way: Wrapper function required
// config.js
let config;
async function loadConfig() {
const response = await fetch('/api/config');
config = await response.json();
}
loadConfig(); // Returns a promise, but we can't await here
export { config }; // config might be undefined when imported!
// ❌ Or using IIFE (Immediately Invoked Function Expression)
(async () => {
const response = await fetch('/api/config');
const config = await response.json();
// Now what? How to export?
})();
With Top-Level Await:
// ✅ New way: Direct top-level await
// config.js
const response = await fetch('/api/config');
const config = await response.json();
export { config }; // config is guaranteed to be loaded
Real-World Use Cases:
// 1. Loading configuration before app starts
// config.js
const response = await fetch('/api/config');
export const config = await response.json();
// 2. Conditional module loading
// feature.js
const isDevelopment = process.env.NODE_ENV === 'development';
const debugModule = isDevelopment
? await import('./debug-tools.js')
: null;
export const debug = debugModule?.debug || (() => {});
// 3. Database connection
// db.js
import { MongoClient } from 'mongodb';
const client = new MongoClient(process.env.DB_URL);
await client.connect();
export const db = client.db('myapp');
console.log('Database connected!');
// 4. Establishing dependencies
// auth.js
const permissions = await fetch('/api/permissions').then(r => r.json());
export const hasPermission = (user, action) => {
return permissions[user]?.includes(action) || false;
};
// 5. Feature detection
// capabilities.js
let wasmSupported = false;
try {
await WebAssembly.instantiate(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]));
wasmSupported = true;
} catch {}
export { wasmSupported };
Module Import Blocking:
// slow-module.js
console.log('Starting slow module');
await new Promise(resolve => setTimeout(resolve, 3000));
console.log('Slow module ready');
export const data = 'Loaded!';
// main.js
console.log('Before import');
import { data } from './slow-module.js'; // Waits for top-level await
console.log('After import:', data);
// Console output:
// "Before import"
// "Starting slow module"
// ... 3 second pause ...
// "Slow module ready"
// "After import: Loaded!"
Execution Order with Multiple Modules:
// a.js
console.log('A: start');
await new Promise(r => setTimeout(r, 100));
console.log('A: end');
export const a = 'A';
// b.js
console.log('B: start');
import { a } from './a.js';
console.log('B: got', a);
export const b = 'B';
// main.js
console.log('Main: start');
import { b } from './b.js';
import { a } from './a.js';
console.log('Main:', a, b);
// Output:
// "A: start"
// (100ms pause)
// "A: end"
// "B: start"
// "B: got A"
// "Main: start"
// "Main: A B"
Error Handling:
// data-loader.js
let data;
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Failed to fetch');
data = await response.json();
} catch (error) {
console.error('Failed to load data:', error);
data = { default: true }; // Fallback data
}
export { data };
Important Considerations:
- Performance: Top-level await blocks the entire module graph, so use sparingly
- Error Impact: If a top-level await rejects and isn't caught, it can prevent module loading
- Not for Scripts: Only works in ES modules (files with
type="module"or.mjsextension) - Circular Dependencies: Be careful with circular imports when using top-level await
// ❌ Don't do this - blocks everything
await new Promise(r => setTimeout(r, 10000)); // 10 second delay!
// ✅ Better approach for initialization
const dataPromise = fetch('/api/data').then(r => r.json());
export const getData = () => dataPromise; // Let consumers decide when to await
Candidate Response Strategy & Interview Tips
- Start with a concise one-sentence summary: Deliver a direct, confident answer first before expanding into nuances.
- Demonstrate real-world trade-offs: Discuss where this approach excels and when you would avoid it in production systems.
- Discuss complexity & edge cases: Proactively explain time/space complexity or boundary conditions (null values, scale limits).
- Prepare for interviewer follow-ups: Technical hiring panels frequently probe deeper into concurrency, backward compatibility, or alternative libraries.