Using Top-Level Await in Bun
Bun supports top-level await, allowing async operations at module scope without wrapping in async functions.
Basic Usage
// database.ts
const db = await connect('postgresql://localhost/mydb');
export { db };
Configuration Loading
// config.ts
const rawConfig = await fs.readFile('config.json', 'utf-8');
const config = JSON.parse(rawConfig);
export const API_KEY = config.apiKey;
export const TIMEOUT = config.timeout || 30000;
Module Initialization
// init.ts
const gemini = await createGeminiClient(process.env.GEMINI_API_KEY);
const youtube = await createYouTubeClient();
export const services = {
gemini,
youtube,
};
Dynamic Imports
// lazy-load-heavy-module.ts
export async function loadHeavyModule() {
const module = await import('./heavy-module');
return module;
}
Sequential Initialization
// services.ts
const step1 = await initializeStep1();
const step2 = await initializeStep2();
const step3 = await initializeStep3();
export const services = { step1, step2, step3 };
Parallel Initialization
// services.ts
const [service1, service2, service3] = await Promise.all([
initializeService1(),
initializeService2(),
initializeService3(),
]);
export const services = { service1, service2, service3 };
Error Handling
// config.ts
let config: Config;
try {
const raw = await fs.readFile('config.json', 'utf-8');
config = JSON.parse(raw);
} catch (error) {
console.error('Failed to load config:', error);
process.exit(1);
}
export { config };
Conditional Loading
// environment.ts
let envConfig: EnvConfig;
if (process.env.NODE_ENV === 'production') {
envConfig = await import('./config/production');
} else {
envConfig = await import('./config/development');
}
export const config = envConfig.default;
Initialization with Timeout
// init.ts
async function initWithTimeout<T>(
promise: Promise<T>,
timeoutMs: number
): Promise<T> {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Initialization timeout')), timeoutMs)
);
return Promise.race([promise, timeout]);
}
const db = await initWithTimeout(connectDatabase(), 5000);
export { db };
Caching Top-Level Results
// cache.ts
let cachedData: Data | null = null;
export async function getData(): Promise<Data> {
if (cachedData) {
return cachedData;
}
cachedData = await fetchData();
return cachedData;
}
// Or with top-level await
const data = await fetchData();
export { data as cachedData };
CLI Entry Point
// index.ts (CLI entry point)
import { Command } from 'commander';
const program = new Command();
// Load config before CLI parsing
const config = await loadConfig();
program
.name('my-cli')
.description('My CLI application')
.version('1.0.0')
.option('-v, --verbose', 'verbose output')
.action((options) => {
if (options.verbose) {
console.log('Config loaded:', config);
}
});
await program.parseAsync(process.argv);
Testing Considerations
// test.ts
import { beforeAll, beforeEach } from 'vitest';
beforeAll(async () => {
// Wait for all modules to initialize
await new Promise(resolve => setTimeout(resolve, 100));
});
beforeEach(async () => {
// Reset module state between tests
vi.resetModules();
});
Best Practices
- Keep initialization fast: Top-level await blocks loading
- Handle errors: Always wrap in try-catch
- Use timeouts: Prevent hanging on slow initialization
- Consider lazy loading: Use dynamic imports for heavy modules
- Document dependencies: Make dependencies explicit
- Test carefully: Top-level await can complicate testing
When to Use
| Scenario | Top-Level Await | Alternative |
|---|---|---|
| Config loading | ✅ Good | - |
| Database connection | ✅ Good | - |
| Heavy computation | ❌ Avoid | Lazy loading |
| CLI entry point | ✅ Good | - |
| Library modules | ❌ Avoid | Async constructors |
| Test setup | ❌ Avoid | beforeAll() hooks |