← Back to Home

How to Use Top-Level Await in Bun

Updated January 14, 2026
buntop-level awaitasyncmodulesesm

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

  1. Keep initialization fast: Top-level await blocks loading
  2. Handle errors: Always wrap in try-catch
  3. Use timeouts: Prevent hanging on slow initialization
  4. Consider lazy loading: Use dynamic imports for heavy modules
  5. Document dependencies: Make dependencies explicit
  6. 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