10.01.2026

JavaScript’te Top-Level Await: Modül Girişinde Asenkron Kurulumun İncelikleri

ES modüllerinde top-level await ile config/SDK yükleme, feature flag ve bootstrap akışını doğru kurgulama.

JavaScript’te çoğu asenkron iş async fonksiyonların içine hapsolur. Oysa ES modülleri sayesinde top-level await ile modülün en üst seviyesinde bekleyebilir, uygulamayı “hazır olana kadar” doğru şekilde başlatabilirsiniz.

Not: Top-level await yalnızca ES module ortamında çalışır (tarayıcıda <script type="module">, Node.js’te ESM).


Ne zaman işe yarar?

Top-level await özellikle “uygulama başlamadan önce” şu işler gerektiğinde güzel bir araçtır:

  • Uzak config (ör. CDN’den config.json) yükleme
  • Feature flag veya A/B testi kuralları çekme
  • WASM / i18n sözlüğü gibi ağır kaynakları yükleyip sonra modülü kullanma
  • SDK başlatma (analytics, ödeme, harita) ve hazır olana kadar bekleme

Bu yaklaşım, “her yerde init().then(...)” yazmak yerine, bağımlılıkları modül sınırında netleştirir.


Temel kullanım

// config.mjs
export const config = await fetch('/config.json').then(r => r.json());

// app.mjs
import { config } from './config.mjs';

console.log('API:', config.apiBase);

Bu örnekte app.mjs, config.mjs tamamlanmadan çalışmaz. Yani modül grafiğinde “doğal bir bekleme” oluşur.


Gerçekçi örnek: Feature flag ile koşullu modül yükleme

Top-level await’ı dinamik import ile birleştirmek, bundle’ı ve başlangıç maliyetini yönetmek için iyi bir tekniktir.

// flags.mjs
export const flags = await fetch('/flags').then(r => r.json());

// bootstrap.mjs
import { flags } from './flags.mjs';

if (flags.newCheckout) {
  const { startNewCheckout } = await import('./checkout-new.mjs');
  startNewCheckout();
} else {
  const { startLegacyCheckout } = await import('./checkout-legacy.mjs');
  startLegacyCheckout();
}

Kazanç: Kullanıcıların bir kısmı hiç kullanmayacağı kodu indirmeyebilir.


Hata yönetimi: Başlangıçta patlamasın

Top-level await’da atılan hata, modül yüklenmesini başarısız yapar. Bu bazen istenir, bazen de “yumuşak düşüş” daha iyidir.

// safe-config.mjs
export const config = await (async () => {
  try {
    const r = await fetch('/config.json');
    if (!r.ok) throw new Error('Config indirilemedi');
    return await r.json();
  } catch (e) {
    // Güvenli varsayılanlar
    return { apiBase: '/api', telemetry: false };
  }
})();

Bu sayede uygulama, config servisi geçici olarak sorun yaşasa bile ayağa kalkabilir.


Dikkat: Bağımlılık zinciri ve “başlangıç gecikmesi”

Top-level await, modül grafiğinde bekleme yarattığı için yanlış yerde kullanılırsa başlangıcı gereksiz uzatır.

Öneriler:

  • Sadece gerçekten başlangıç için gerekli işleri top-level await ile yapın.
  • “UI hemen gelsin, veri sonra aksın” gibi senaryolarda asenkron işi içeride başlatıp suspense benzeri bir akış kurun.
  • Uzun işler için timeout + fallback düşünün.

Basit timeout örneği:

const withTimeout = (p, ms) =>
  Promise.race([
    p,
    new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms))
  ]);

export const config = await withTimeout(
  fetch('/config.json').then(r => r.json()),
  1500
).catch(() => ({ apiBase: '/api' }));


Node.js tarafı: ESM ve test edilebilirlik

Node’da ESM kullanıyorsanız (ör. "type": "module"), top-level await ile DB bağlantısı gibi işleri modül girişine koymak cazip gelebilir. Ancak testlerde “import edince bağlanıyor” durumu istenmeyebilir.

Pratik yaklaşım:

  • Top-level await’ı salt veri/konfig gibi yan etkisi düşük şeylerde kullanın.
  • Yan etkili işleri (DB connect, server listen) mümkünse bir start() fonksiyonunda başlatın.

Sonuç

Top-level await, JavaScript modül sistemini daha ifade gücü yüksek hale getiriyor: bağımlılıkları “init zincirleri” yerine modül sınırında tanımlıyorsunuz. Doğru kullanıldığında kodu sadeleştirir; yanlış kullanıldığında ise başlangıcı yavaşlatabilir. Kural basit: kritik bootstrap için kullan, geri kalanını akış içinde yönet.