Mert Özen Her Satırda Daha İleri
Frontend JavaScript İleri — #07

JavaScript Async Iterator: for await...of, Async Generator ve Array.fromAsync

Mert Özen 3 Kas 2026 9 dk 3 görüntülenme
JavaScript Async Iterator: for await...of, Async Generator ve Array.fromAsync

Symbol.asyncIterator ve async generator ile zaman içinde gelen veriyi geziyor, for await...of ve ES2026'nın Array.fromAsync'ini öğreniyor, sayfalı API'yi akış olarak okuyup ReadableStream'e giriş yapıyoruz.

Sürüm notu: Bu yazıdaki kodlar ES2026 ve güncel tarayıcılar (Chrome, Edge, Firefox, Safari) içindir.

Önce Tahmin Et

Daha önce gördüğümüz generator'ların başına bir async ekleyelim ve sonucu alışık olduğumuz döngüyle gezmeye çalışalım. Bu kod konsola ne yazdırır?

async function* numbers() {
  yield 1;
  yield 2;
}

console.log(numbers().next() instanceof Promise);

for (const n of numbers()) {
  console.log(n);
}

Okumaya devam etmeden önce bir tahmin yap. Küçük bir ipucu: iki kısmın cevabı birbirine bağlı. Cevabı async iterator protokolünü kurduktan sonra vereceğiz.

Senkron Iterator Nerede Tıkanır?

Daha önce gördüğümüz iterator protokolünü hatırla: next() çağırırsın, karşılığında hemen { value, done } şeklinde bir object alırsın. Buradaki kilit kelime "hemen". for...of her turda next()'i çağırır ve cevabın o anda elinde olmasını bekler. Bir array, bir Set ya da bellekte duran bir generator için bu hiç sorun değil.

Peki sıradaki değer henüz ortada yoksa? Bir API'nin ikinci sayfası, bir dosyanın daha inmemiş bir parçası, sunucudan gelecek bir sonraki mesaj... Senkron iterator bunları bekleyemez; next() o anda bir şey döndürmek zorunda. Bir promise döndürse bile for...of onu açmaz, promise'in kendisini değer diye eline tutuşturur.

Bunu bir restoranla düşün. Senkron iterator açık büfe gibidir: tabağını uzatırsın, yemek zaten tezgâhtadır, hemen alırsın. Async iterator ise garsonlu servis. "Sıradaki tabak" dersin, garson "hemen getiriyorum" deyip mutfağa gider. Sen masada beklersin ama restoranın geri kalanı durmaz. Garsonun verdiği o söz, JavaScript'teki karşılığıyla bir promise'tir.

Async Iterator Protokolü ve Symbol.asyncIterator

ES2018 ile gelen async iteration, senkron protokolün neredeyse aynısıdır. Tek fark, next()'in bir promise döndürmesi ve bu promise'in { value, done } object'iyle çözülmesidir. Bir object'in async iterable sayılması için de Symbol.iterator yerine Symbol.asyncIterator anahtarında böyle bir iterator döndüren bir method taşıması gerekir.

Elle yazılmış küçük bir geri sayımla görelim. Bunu tarayıcının konsoluna yapıştırıp dene:

const countdown = {
  [Symbol.asyncIterator]() {
    let left = 3;
    return {
      next() {
        return new Promise((resolve) => {
          setTimeout(() => {
            if (left === 0) {
              resolve({ value: undefined, done: true });
            } else {
              resolve({ value: left--, done: false });
            }
          }, 500);
        });
      }
    };
  }
};

for await (const n of countdown) {
  console.log(n);
}
console.log("Liftoff!");

Yarım saniye arayla 3, 2, 1 ve en sonda Liftoff! göreceksin. Motorun yaptığı şu: for await...of önce countdown[Symbol.asyncIterator]() ile iterator'ı alır. Sonra her turda next()'i çağırır, dönen promise'i await eder ve done değeri true olana kadar bunu tekrarlar. Döngü beklerken call stack boşalır, event loop başka işlere bakar; sayfa donmaz.

Küçük bir not: konsol top-level await'e izin verdiği için for await'i doğrudan yazabildik. Klasik bir <script> içinde bu döngüyü bir async fonksiyonun içine koyman gerekir. Module script'lerinde ise daha önce gördüğümüz top-level await sayesinde dışarıda da çalışır.

Async Generator: async function*

Elle iterator yazmak öğreticidir ama yorucudur. Senkron tarafta generator'ların bu işi nasıl kısalttığını görmüştük. Async tarafta da aynı kolaylık var: async function*. Gövdesinde hem await hem yield kullanabilirsin. Aynı geri sayımı bir de böyle yaz:

const wait = (ms) => new Promise((r) => setTimeout(r, ms));

async function* countDown(start) {
  for (let i = start; i > 0; i--) {
    await wait(500);
    yield i;
  }
}

for await (const n of countDown(3)) {
  console.log(n);
}

Yirmi satıra yakın object birkaç satıra indi. Async generator çağrıldığında gövdesi hemen çalışmaz; senkron generator'larda olduğu gibi duraklatılmış bir object döner. Bu object hem async iterator'dır hem de async iterable, yani Symbol.asyncIterator method'u kendisini döndürür. Her next() çağrısı gövdeyi bir sonraki yield'e kadar ilerletir ve o değeri bir promise içinde teslim eder.

Artık baştaki soruya dönebiliriz. numbers().next() bir promise döndürür, bu yüzden ilk satır true yazar. Döngü ise hata verir: TypeError: ... is not iterable. Async generator object'inde Symbol.asyncIterator var ama Symbol.iterator yok; for...of aradığı kapıyı bulamaz. for'un yanına await eklediğinde 1 ve 2 sorunsuz gelir.

for await...of'un İncelikleri

for await...of yalnızca async iterable'larla çalışmaz. Ona senkron bir iterable verirsen, örneğin promise'lerden oluşan bir array'i, her element'i sırayla await eder. Bir önceki örnekteki wait fonksiyonunu kullanıyoruz:

const requests = [
  wait(300).then(() => "users"),
  wait(100).then(() => "products"),
  wait(200).then(() => "orders")
];

for await (const result of requests) {
  console.log(result);
}

Çıktı her seferinde users, products, orders sırasıyla gelir; hangisinin önce bittiği önemli değil. Üç promise de array'i kurduğumuz anda başladığı için toplam süre yaklaşık 300 ms. Ama döngü sırayı korur: birinci element çözülmeden ikinciye geçmez.

Bir de erken çıkış meselesi var. Döngüden break, return ya da bir hatayla çıktığında motor iterator'ın return() method'unu çağırır. Async generator'da bu, gövdedeki finally bloğunun çalışması demek. Bağlantı kapatmak, zamanlayıcı temizlemek gibi işleri oraya koyarsın:

async function* sensor() {
  try {
    let i = 0;
    while (true) {
      await wait(200);
      yield ++i;
    }
  } finally {
    console.log("Sensor closed");
  }
}

for await (const reading of sensor()) {
  console.log("Reading:", reading);
  if (reading === 3) break;
}

Sonsuz bir while (true) yazdık ama endişelenme: generator yalnızca sen istedikçe ilerler. Üçüncü ölçümden sonra break gelir, finally çalışır ve konsolda Sensor closed görünür.

Array.fromAsync ile Hepsini Toplamak

Bazen akışın tamamını tek bir array'de istersin. Senkron tarafta bunun için Array.from var. ES2026 ile gelen Array.fromAsync aynı işi async iterable'lar için yapar ve bir promise döndürür:

const list = await Array.fromAsync(countDown(3));
console.log(list); // [3, 2, 1]

const squares = await Array.fromAsync(countDown(3), (n) => n * n);
console.log(squares); // [9, 4, 1]

İkinci argüman, Array.from'daki gibi bir map fonksiyonu. Array.fromAsync standarda ES2026'da girse de tarayıcılara çok daha önce geldi: Chrome 121, Firefox 115 ve Safari 16.4'ten beri var ve Temmuz 2026'dan beri Baseline'da "geniş çapta destekleniyor" durumunda. Gönül rahatlığıyla kullanabilirsin.

Akılda tutman gereken fark şu: Array.fromAsync element'leri sırayla, birer birer bekler; Promise.all ise hepsini aynı anda bekler. Elinde zaten başlamış promise'lerden oluşan bir array varsa ikisi de benzer sürede biter. Ama element'ler bir async generator'dan tek tek üretiliyorsa Array.fromAsync, öncekisi gelmeden bir sonrakini istemez. Bu tembellik çoğu zaman tam aradığın şeydir.

Sayfalı Veriyi Akış Olarak Okumak

Async generator'ın en çok parladığı yer sayfalı API'ler. Sunucu veriyi yirmişer yirmişer veriyor; sen ise "bana gönderileri tek tek ver, sayfa işini sen hallet" demek istiyorsun. Aşağıdaki örnek CORS'a izin veren, herkese açık bir test API'si kullanıyor. Bazı siteler kendi güvenlik kurallarıyla dışarıya isteği engelleyebildiği için bunu boş bir sekmede (about:blank) açtığın konsolda dene:

async function* posts() {
  let page = 1;
  while (true) {
    const url = `https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=20`;
    console.log("Sending request, page:", page);
    const response = await fetch(url);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const items = await response.json();
    if (items.length === 0) return;
    yield* items;
    page++;
  }
}

for await (const post of posts()) {
  if (post.title.includes("sed ab")) {
    console.log("Found:", post.id, post.title);
    break;
  }
}

Konsolda kaç istek atıldığına bak. Aradığımız gönderi üçüncü sayfada olduğu için yalnızca üç istek gider; kalan sayfalar hiç istenmez. Tüketen taraf sayfa numarasından, URL'den, son sayfa kontrolünden habersiz; sadece gönderileri geziyor. yield* ise daha önce gördüğümüz gibi bir iterable'ın element'lerini tek tek dışarı verir; burada bir sayfanın array'ini akışa döküyor.

ReadableStream'e Giriş

Tarayıcının içinde de hazır async iterable'lar var. En önemlisi Streams API'sindeki ReadableStream. fetch ile aldığın yanıtın body property'si bir ReadableStream'dir: yanıt tamamen inmeden, geldikçe parça parça (chunk) okuyabilirsin. Her parça bir Uint8Array, yani ham byte'lar. İkili veriye ilerleyen yazılarda ayrıca gireceğiz; şimdilik "byte array'i" demek yeterli.

const response = await fetch("https://jsonplaceholder.typicode.com/photos");
let total = 0;
let chunkCount = 0;

for await (const chunk of response.body) {
  total += chunk.length;
  chunkCount++;
}
console.log(`${chunkCount} chunks, ${total} bytes in total`);

Kendi stream'ini de kurabilirsin. start fonksiyonuna gelen controller ile queue'ya değer ekler (enqueue), işin bitince akışı kapatırsın (close):

const stream = new ReadableStream({
  start(controller) {
    controller.enqueue("Hello");
    controller.enqueue("stream");
    controller.close();
  }
});

for await (const chunk of stream) {
  console.log(chunk);
}

Metin okuyorsan byte'larla uğraşmak yerine stream'i bir TextDecoderStream'den geçirebilirsin: response.body.pipeThrough(new TextDecoderStream()) sana doğrudan string parçaları verir.

Destek konusunda açık konuşalım. ReadableStream'in kendisi yıllardır her yerde var, ama onu for await ile gezebilmek en son Safari'ye geldi: özellik Safari 27 ile Eylül 2026'da Baseline'a girdi, Chrome 124 ve Firefox 110 ise çok daha önce destekliyordu. Eski Safari sürümlerini de düşünmen gerekiyorsa klasik yol hâlâ geçerli; bir reader alıp done gelene kadar okursun:

const reader = (await fetch("https://jsonplaceholder.typicode.com/posts")).body.getReader();

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  console.log("Chunk:", value.length, "bytes");
}

Sık Düşülen Tuzaklar

En sık hata await'i unutmak. Baştaki örnekte gördüğün gibi async generator'ı for...of ile gezersen "is not iterable" hatası alırsın. Tersi ise sessiz kalır: senkron bir iterable'ı for await ile gezmek çalışır, sadece her adıma gereksiz bir bekleme ekler.

İkincisi, sıralı beklemeyi paralel sanmak. for await bir adımı bitirmeden diğerine geçmez. Birbirinden bağımsız on isteği async generator içinde tek tek await edersen on katı süre beklersin. Bağımsız işler için daha önce gördüğümüz Promise.all hâlâ doğru araç. Async iteration, sıranın ya da tembelliğin önemli olduğu yerde işe yarar.

Üçüncüsü, iterator helper'ları async tarafta aramak. Daha önce gördüğümüz map, filter, take gibi helper'lar yalnızca senkron iterator'larda var. countDown(3).map(...) yazarsan TypeError alırsın; async karşılıkları TC39'da hâlâ öneri aşamasında. Şimdilik dönüşümü kendi async generator'ınla yaparsın:

async function* filter(source, predicate) {
  for await (const x of source) {
    if (predicate(x)) yield x;
  }
}

console.log(await Array.fromAsync(filter(countDown(5), (n) => n % 2 === 1)));
// [5, 3, 1]

Son olarak, bir stream'i iki kez okumaya çalışmak. ReadableStream'i gezerken stream kilitlenir; aynı anda ikinci bir okuyucu TypeError alır. Döngüden break ile çıktığında da stream varsayılan olarak iptal edilir ve bir daha okunamaz. Okumaya sonra devam etmek istiyorsan for await (const c of stream.values({ preventCancel: true })) yazman gerekir. Bir yanıtın gövdesi de tek kullanımlıktır: body'yi gezdikten sonra response.json() çağıramazsın.

Küçük Bir Deneme

★☆☆ wait fonksiyonunu kullanarak her saniye bir sonraki harfi veren async function* letters(word) yaz ve for await ile "JavaScript" kelimesini harf harf konsola dök.

★★☆ Çalıştırmadan önce çıktıyı tahmin et: for await (const x of [1, wait(100).then(() => 2), 3]) console.log(x); Promise olmayan 1 ve 3 ne zaman yazılır, sıra değişir mi? Sonra konsolda dene.

★★★ Bir async iterable ve bir n sayısı alan async function* firstN(source, n) yaz; n element verdikten sonra dursun. posts generator'ına bir finally bloğu ekle, sonra firstN(posts(), 5) ile dene. Konsolda yalnızca bir istek atıldığını ve finally'nin çalıştığını doğrula.

Bu yazıda Symbol.asyncIterator gibi özel bir anahtarla motora "beni böyle gez" dedik; yani dilin bir protokolüne kendi davranışımızı taktık. Bir sonraki yazıda bunu bir adım ileri taşıyacağız ve bir object'e yapılan her okuma, yazma ve silme işlemini yolda yakalamayı öğreneceğiz: Proxy ve Reflect.