Skip to content

Bölüm 17 / 20

Asenkron JavaScript

callback, Promise, async/await, fetch.

14 dakikalık okuma

JavaScript tek iş parçacığında çalışır: aynı anda tek bir şey yapar. Ağ isteği, dosya okuma ve zamanlayıcı gibi beklemeli işler bu yüzden asenkrondur — sonuç geldiğinde çalışacak kod sıraya konur, arada arayüz donmaz. Bu ders o mekanizmayı (olay döngüsü), üç kuşak yazım biçimini (geri çağrı, Promise, async/await) ve tarayıcının ağ arayüzü fetch'i ele alıyor. 6. hatta yazdığın API'yi burada tüketeceksin; 1. derste konuştuğumuz istek/yanıt zincirinin kod tarafı burasıdır.

01Ne öğreneceksin

  • Tek iş parçacığı, çağrı yığını ve olay döngüsü
  • Görev kuyruğu ile mikro görev kuyruğu farkı
  • Geri çağrı (callback) yazımı ve "geri çağrı cehennemi"
  • Promise: üç durum, zincirleme, catch/finally
  • async/await ve try/catch ile hata yönetimi
  • Sıralı mı paralel mi — await'i döngüde kullanmanın maliyeti
  • Promise.all, allSettled, race, any: hangisi ne zaman
  • forEach içinde await'in neden çalışmadığı
  • fetch'in 404'te hata fırlatmaması ve doğru kontrol
  • Zaman aşımı, iptal (AbortController) ve yeniden deneme
  • Yarış koşulu: geç gelen eski yanıtın yeni sonucu ezmesi

02Tek iş parçacığı ve olay döngüsü

JavaScript'in tek bir çağrı yığını vardır. Uzun süren bir hesap yaparsan sayfa donar: tıklamalar işlenmez, animasyonlar durur, kaydırma takılır. Beklemeli işleri asenkron yapan şey dilin kendisi değil, çalışma ortamıdır: ağ isteğini tarayıcı üstlenir, bitince geri çağrıyı bir kuyruğa koyar.

Çağrı yığını boşaldığında olay döngüsü sırayla şunu yapar:

  1. MİKRO GÖREV kuyruğunu TAMAMEN boşalt     ← Promise geri çağrıları, queueMicrotask
  2. Bir GÖREV al ve çalıştır                  ← setTimeout, olay işleyicileri, ağ yanıtları
  3. Gerekiyorsa ekranı çiz (render)
  4. Başa dön

Bu sıralamanın somut sonucu:

console.log("1");
setTimeout(() => console.log("2 — görev"), 0);
Promise.resolve().then(() => console.log("3 — mikro görev"));
console.log("4");

// Çıktı: 1, 4, 3, 2
// setTimeout(…, 0) "hemen" demek değildir: sıradaki GÖREV demektir ve
// tüm mikro görevler ondan ÖNCE çalışır.

İki pratik sonuç: (1) setTimeout(fn, 0) işi "bir sonraki tura ertelemek" için kullanılır, hızlandırmak için değil; (2) sonsuz zincirlenen mikro görevler ekranın hiç çizilmemesine yol açabilir — uzun hesapları parçalamak istiyorsan setTimeout ya da requestIdleCallback gerekir.

Gerçekten ağır bir hesap (büyük veri işleme, şifreleme) varsa doğru araç Web Worker'dır: ayrı bir iş parçacığında çalışır, DOM'a erişemez, ana iş parçacığıyla mesaj alışverişi yapar. 1. derste gördüğün INP metriğini kurtaran şey çoğu zaman budur.

03Geri çağrı: birinci kuşak

function veriGetir(id, tamamlandi) {
  setTimeout(() => tamamlandi(null, { id, ad: "Ayşe" }), 500);
  //                             ^ Node.js geleneği: ilk parametre HATA
}

veriGetir(1, (hata, veri) => {
  if (hata) return göster(hata);
  veriGetir(veri.ustId, (hata2, ust) => {        // iç içe geçmeye başlıyor
    if (hata2) return göster(hata2);
    veriGetir(ust.ustId, (hata3, kok) => {       // "geri çağrı cehennemi"
      …
    });
  });
});

Sorunları: iç içe geçme okunmaz hâle gelir, hata her seviyede elle taşınır, paralel işleri koordine etmek elle sayaç tutmayı gerektirir ve try/catch asenkron sınırı geçemez. Promise bu üç sorunu da çözmek için geldi.

04Promise

Bir Promise, gelecekte bir değer üreteceği sözü verilen nesnedir. Üç durumu vardır: bekliyor (pending), gerçekleşti (fulfilled), reddedildi (rejected). Bir kez sonuçlandıktan sonra durumu değişmez.

const soz = new Promise((coz, reddet) => {
  setTimeout(() => coz("bitti"), 500);          // ya da reddet(new Error("olmadı"))
});

soz
  .then(v => { console.log(v); return v.length; })   // dönen değer SONRAKİ then'e gider
  .then(u => console.log(u))
  .catch(h => console.error(h))                     // zincirin HERHANGİ bir yerindeki hata
  .finally(() => gostergecKapat());                 // her durumda çalışır

Zincirlemenin kuralı: then içinde bir Promise döndürürsen zincir onu bekler. En sık yapılan hata return yazmayı unutmaktır — zincir beklemez ve hatalar catch'e ulaşmaz.

new Promise'i elle yazmak nadiren gerekir; çoğunlukla zaten Promise döndüren API'leri (fetch) kullanırsın. Elle yazman gereken tipik yer, eski bir geri çağrılı API'yi sarmalamaktır.

05async / await

async function basvuruGetir(id) {          // async fonksiyon HER ZAMAN Promise döndürür
  try {
    const yanit = await fetch(`/api/basvuru/${id}`);
    if (!yanit.ok) throw new Error(`HTTP ${yanit.status}`);
    return await yanit.json();
  } catch (h) {
    kaydet(h);
    throw h;                                // yutma — çağırana bildir
  } finally {
    gostergecKapat();
  }
}

await, Promise sonuçlanana kadar o fonksiyonun çalışmasını durdurur; sayfayı dondurmaz — arada olay döngüsü diğer işleri yapar. Kazancı, asenkron kodun senkron gibi okunması ve try/catch'in çalışmasıdır.

await yalnız async fonksiyon içinde kullanılır; istisnası ES modüllerinin en üst seviyesidir (18. ders).

Sıralı mı, paralel mi

Bu, async/await'in en pahalı tuzağıdır. Arka arkaya yazılan her await bir öncekini bekler; birbirine bağlı değillerse bu boşa geçen zamandır.

// SIRALI — 3 istek × 300 ms = 900 ms
const a = await getir("/api/kullanici");
const b = await getir("/api/ayarlar");
const c = await getir("/api/duyurular");

// PARALEL — hepsi aynı anda başlar ≈ 300 ms
const [a, b, c] = await Promise.all([
  getir("/api/kullanici"),
  getir("/api/ayarlar"),
  getir("/api/duyurular")
]);

Sıralı yazmak yalnız bir sonraki istek bir öncekinin sonucuna ihtiyaç duyuyorsa doğrudur.

BirleştiriciDavranışNe zaman
Promise.allBiri reddedilirse hemen reddeder; diğerleri iptal edilmez, sonuçları atılırHepsi başarılı olmak zorundaysa
Promise.allSettledHepsini bekler; her biri için {status, value/reason} verirKısmi sonuç gösterilecekse (6. dersteki "kısmi" durumu)
Promise.raceİlk sonuçlanan (başarı ya da hata) kazanırZaman aşımı kurmak
Promise.anyİlk başarılı olan kazanır; hepsi başarısızsa AggregateErrorYedekli kaynaklar

forEach içinde await çalışmaz — 13. derste bırakılan senedin karşılığı budur:

// YANLIŞ — forEach geri çağrının Promise'ini beklemez; döngü ANINDA biter
kayitlar.forEach(async (k) => { await kaydet(k); });
console.log("bitti");                     // hiçbiri kaydedilmeden yazılır

// DOĞRU — sıralı gerekiyorsa
for (const k of kayitlar) { await kaydet(k); }

// DOĞRU — paralel olabilirse
await Promise.all(kayitlar.map(k => kaydet(k)));

Paralelde bir uyarı: 500 kaydı aynı anda göndermek sunucuyu ve tarayıcının bağlantı havuzunu zorlar. Büyük listelerde parti parti ilerlemek ya da eşzamanlılık sınırı koymak gerekir.

06fetch

const yanit = await fetch("/api/basvurular?durum=acik", {
  method: "GET",
  headers: { "Accept": "application/json" },
  credentials: "same-origin"        // çerez politikası (1. ders)
});

if (!yanit.ok) { … }                // 200-299 dışı
const veri = await yanit.json();    // gövdeyi OKUMAK da asenkrondur

fetch HTTP hatalarında reddetmez

Bu, fetch'in en çok şaşırtan davranışıdır: sunucu 404 ya da 500 döndürdüğünde Promise başarıyla sonuçlanır. fetch yalnız ağ düzeyinde bir sorun olduğunda reddeder — bağlantı kurulamadı, istek iptal edildi, CORS engelledi. Dolayısıyla catch bloğuna güvenmek yetmez; yanit.ok kontrolü elle yazılır. Bunu unutmak, sunucudan gelen hata gövdesini geçerli veri sanıp undefined alanlarla ilerlemeye yol açar. (Axios gibi kütüphanelerin varsayılan olarak hata fırlatmasının sebebi de budur.)

async function istek(yol, secenek = {}) {
  const yanit = await fetch(yol, {
    headers: { "Accept": "application/json", ...secenek.headers },
    ...secenek
  });

  if (!yanit.ok) {
    // 6. hattaki hata sözleşmesi: sunucu problem+json döndürüyor
    let ayrinti = null;
    try { ayrinti = await yanit.json(); } catch { /* gövde JSON değil */ }
    throw new ApiHatasi(yanit.status, ayrinti?.detail ?? yanit.statusText, ayrinti);
  }

  if (yanit.status === 204) return null;        // No Content — json() PATLAR
  return yanit.json();
}

class ApiHatasi extends Error {
  constructor(durum, mesaj, govde) { super(mesaj); this.durum = durum; this.govde = govde; }
}

Gövde bir kez okunur: yanit.json()'ı iki kez çağıramazsın (akış tükenir). İhtiyaç varsa yanit.clone() kullanılır.

// Gönderme
await istek("/api/basvurular", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(veri)
});

// Dosya: Content-Type'ı YAZMA (3. ders — boundary tarayıcıdan gelir)
await fetch("/api/belge", { method: "POST", body: new FormData(form) });

Zaman aşımı, iptal ve yeniden deneme

fetch'in yerleşik zaman aşımı yoktur; kendin kurarsın:

const kontrol = new AbortController();
const zaman = setTimeout(() => kontrol.abort(), 10_000);

try {
  const y = await fetch(yol, { signal: kontrol.signal });
} catch (h) {
  if (h.name === "AbortError") { /* zaman aşımı ya da kullanıcı iptali */ }
} finally {
  clearTimeout(zaman);
}

// Kısa yol
await fetch(yol, { signal: AbortSignal.timeout(10_000) });

Yeniden deneme bir karardır ve 6. derste konuştuğumuz kurala bağlıdır: yalnız idempotent istekler (okuma) otomatik tekrarlanır. Yazma isteğini kendiliğinden tekrarlamak, 3. derste gördüğün çift kayıt sorununu üretir — orada idempotentlik anahtarı gerekir. Tekrarlanacaksa üstel geri çekilme uygulanır: 1 sn, 2 sn, 4 sn… ve 429/503 yanıtındaki Retry-After başlığına uyulur.

async function tekrarli(fn, deneme = 3) {
  for (let i = 0; i < deneme; i++) {
    try { return await fn(); }
    catch (h) {
      const gecici = h.durum === 429 || h.durum >= 500 || h.name === "TypeError";
      if (!gecici || i === deneme - 1) throw h;
      await new Promise(r => setTimeout(r, 2 ** i * 1000));
    }
  }
}

07Yarış koşulu: geç gelen eski yanıt

Arama kutusuna hızlı yazan bir kullanıcı üç istek başlatır. Yanıtlar gönderildikleri sırayla gelmez; ikinci istek üçüncüden sonra dönerse ekranda eski sonuç kalır. Bu, ön yüzün en sinsi hatalarından biridir çünkü geliştirici makinesinde neredeyse hiç görülmez.

let sonIstek = 0;
let iptal;

async function ara(sorgu) {
  iptal?.abort();                        // 1. önceki isteği İPTAL et
  iptal = new AbortController();
  const benim = ++sonIstek;              // 2. sıra numarası ver

  try {
    const sonuc = await istek(`/api/ara?q=${encodeURIComponent(sorgu)}`,
                              { signal: iptal.signal });
    if (benim !== sonIstek) return;      // 3. ben en son istek değilsem SONUCU AT
    ciz(sonuc);
  } catch (h) {
    if (h.name !== "AbortError") hataGoster(h);
  }
}

Üç önlem birlikte kullanılır: iptal gereksiz ağ trafiğini keser, sıra numarası iptal edilemeyen durumlarda son yanıtı garantiler, debounce (14. ders) istek sayısını baştan azaltır.

Hataları kullanıcıya göstermek

6. derste bir ekranın beş durumu olduğunu söylemiştik; asenkron kodun görevi bu durumları doğru sürmektir:

async function listeYukle() {
  durumGoster("yukleniyor");
  try {
    const veri = await istek("/api/basvurular");
    durumGoster(veri.length ? "dolu" : "bos", veri);
  } catch (h) {
    if (h.name === "AbortError") return;             // iptal hata değildir
    durumGoster("hata", h.durum === 403
      ? "Bu listeyi görme yetkiniz yok."
      : "Liste yüklenemedi. Tekrar deneyin.");
    bildir(h);                                        // günlüğe teknik ayrıntı
  }
}

// Yakalanmayan reddedilmeler için ağ:
window.addEventListener("unhandledrejection", (e) => bildir(e.reason));

unhandledrejection, catch'i unutulmuş Promise'leri yakalar; üretimde hata izleme aracına bağlanan yer burasıdır. Konsolda "Uncaught (in promise)" uyarısını görüyorsan bir zincirde catch ya da return eksiktir.

08Mini alıştırma — Gerçek bir istek katmanı yaz

Yaklaşık 20 dakika · tarayıcı konsolu; herkese açık bir test API'si ya da kendi 6. hat servisin

  1. Sıra: 2. bölümdeki dört satırlık kodu çalıştır ve çıktı sırasını açıkla.
  2. Görev vs mikro görev: setTimeout(…, 0) ile queueMicrotask'i art arda koyup hangisinin önce çalıştığını gör.
  3. Promise zinciri: üç then'li bir zincir yaz; ortadakinde return yazmayı unut ve sonucun ne olduğuna bak.
  4. catch yeri: zincirin ortasında hata fırlat; catch'i sona koyduğunda ve then'ler arasına koyduğunda ne değişiyor?
  5. Sıralı vs paralel: 300 ms geciken üç sahte isteği önce arka arkaya await, sonra Promise.all ile çağır. console.time ile ölç.
  6. all vs allSettled: üç istekten birini bilerek reddet. Hangisinde diğer sonuçlara erişebildin?
  7. forEach tuzağı: 13. dersteki senedi kapat — forEach(async …) ile beş kaydı kaydet ve sonunda "bitti" yaz. Sıra doğru mu? for...of ve Promise.all ile tekrarla.
  8. fetch ve 404: var olmayan bir yola istek at. catch çalıştı mı? yanit.ok ve yanit.status değerlerini yazdır.
  9. 204: gövdesiz bir yanıtta yanit.json() çağır. Hangi hata geldi?
  10. Gövde bir kez okunur: aynı yanıtta json()'ı iki kez çağır. clone() ile düzelt.
  11. Zaman aşımı: yavaş bir uç noktaya AbortSignal.timeout(1000) ile istek at ve AbortError'ı yakala.
  12. Yeniden deneme: tekrarli yardımcısını yaz; ilk iki denemede hata üreten sahte bir fonksiyonla test et. Bekleme süreleri katlanıyor mu?
  13. Yarış koşulu: her çağrıda rastgele 100-1500 ms geciken sahte bir arama fonksiyonu yaz; hızlıca üç farklı sorgu gönder. Ekranda hangi sonuç kaldı? Sıra numarası ve iptal ekleyip tekrarla.
  14. Beş durum: yükleniyor / boş / hata / kısmi / dolu durumlarını gerçekten süren bir listeYukle yaz (6. dersteki ekranı yeniden kullanabilirsin).
  15. Yakalanmayan reddetme: catch'siz bir zincir bırak ve unhandledrejection dinleyicisinde yakala.

09Kendini sına

JavaScript tek iş parçacıklı olduğu hâlde asenkron işleri nasıl yapar? Olay döngüsü nedir?

Dilin tek bir çağrı yığını vardır; aynı anda tek bir kod parçası çalışır. Beklemeli işleri dil değil çalışma ortamı üstlenir: ağ isteğini tarayıcı yapar, zamanlayıcıyı tarayıcı sayar; iş bitince ilgili geri çağrıyı bir kuyruğa koyar. Olay döngüsü, yığın boşaldığında bu kuyruklardan iş alıp çalıştıran mekanizmadır. İki kuyruk vardır ve sırası önemlidir: önce mikro görev kuyruğu tamamen boşaltılır (Promise geri çağrıları, queueMicrotask), sonra bir makro görev alınır (setTimeout, olay işleyicileri, ağ yanıtları), ardından gerekiyorsa ekran çizilir. Bu yüzden setTimeout(fn, 0) "hemen" demek değildir — sıradaki görev demektir ve tüm mikro görevler ondan önce çalışır. Pratik sonuçlar: (1) uzun süren senkron bir hesap her şeyi dondurur, çünkü olay döngüsü sıraya gelemez — bunun çözümü işi parçalamak ya da Web Worker kullanmaktır; (2) sonsuz zincirlenen mikro görevler ekranın hiç çizilmemesine yol açabilir; (3) await sayfayı dondurmaz, yalnız o fonksiyonun devamını erteler.

Promise.all, allSettled, race ve any arasındaki farklar nelerdir?

all hepsinin başarılı olmasını bekler; biri reddedilirse hemen reddeder ve o ana kadar gelen sonuçlara erişemezsin (diğer istekler iptal edilmez, sonuçları atılır). "Hepsi olmadan sayfa anlamsız" durumlarında doğrudur. allSettled hiçbir zaman reddetmez; her biri için {status: "fulfilled"|"rejected", value|reason} döndürür — kısmi sonuç göstermek istediğinde (bir servis çöktüğünde diğer verileri yine de göstermek) doğru araç budur ve 6. derste konuştuğumuz "kısmi" ekran durumunun karşılığıdır. race ilk sonuçlananı alır — başarı da olabilir hata da; klasik kullanımı bir isteği zaman aşımı Promise'iyle yarıştırmaktır (bugün AbortSignal.timeout daha doğru bir çözümdür). any ilk başarılı olanı alır, hatalar yok sayılır; hepsi başarısız olursa AggregateError fırlatır — yedekli kaynaklardan (aynala) ilk yanıtı almak için kullanılır. Ortak uyarı: bu birleştiricilerin hiçbiri diğer istekleri iptal etmez; gerçekten durdurmak için AbortController gerekir. Ayrıca çok sayıda isteği aynı anda başlatmak sunucuyu ve tarayıcı bağlantı havuzunu zorlar; büyük listelerde eşzamanlılığı sınırlamak gerekir.

fetch 404 döndüğünde neden catch çalışmaz? Doğru hata yönetimi nasıl yazılır?

Çünkü fetch için bir yanıt almak başarıdır; yanıtın durum kodunun ne olduğu onun sorunu değildir. Promise yalnız ağ düzeyinde bir sorun olduğunda reddeder: bağlantı kurulamadı, DNS çözülemedi, istek iptal edildi (AbortError) ya da CORS engelledi. 404 ve 500 gayet geçerli yanıtlardır, dolayısıyla then/await dalına düşerler. Doğru yazım if (!yanit.ok) throw … kontrolünü elle eklemektir — ok, durum kodunun 200-299 aralığında olduğunu söyler. İyi bir istek sarmalayıcısı ayrıca şunları yapar: hata gövdesini okuyup (6. hattaki problem+json sözleşmesi) kullanıcıya anlamlı mesaj çıkarır; durum kodunu taşıyan özel bir hata sınıfı fırlatır (403 ile 500'e farklı tepki verebilmek için); 204 No Content yanıtında json() çağırmaz (gövde yoktur, hata fırlatır); gövdenin bir kez okunabildiğini bilir (clone() gerekirse). Son olarak fetch'in yerleşik zaman aşımı yoktur; AbortSignal.timeout() ile eklenir — yoksa istek dakikalarca asılı kalabilir.

Arama kutusunda yazarken bazen eski sonuçlar ekranda kalıyor. Sebep ve çözüm nedir?

Sebep yarış koşuludur: hızlı yazan kullanıcı arka arkaya istekler başlatır ve yanıtlar gönderildikleri sırayla dönmez. İkinci istek üçüncüden sonra tamamlanırsa, ekrana en son yazılan sonuç eski sorgunun sonucudur. Geliştirici makinesinde ağ hızlı olduğu için neredeyse hiç görülmez; gerçek kullanıcılarda düzenli olarak ortaya çıkar. Çözüm üç katmanlıdır: (1) debounce — her tuşta değil, kullanıcı durakladığında (≈300 ms) istek at; bu istek sayısını baştan azaltır. (2) İptal — yeni istek başlarken öncekini AbortController.abort() ile kes; hem ağ trafiği hem sunucu yükü azalır ve iptal edilen istek AbortError ile gelir (bunu kullanıcıya hata olarak gösterme). (3) Sıra numarası — her isteğe artan bir numara ver ve yanıt geldiğinde if (benim !== sonIstek) return; ile eski yanıtı at; iptalin mümkün olmadığı ya da yarışın başka bir katmandan geldiği durumlar için güvencedir. Aynı desen sekme değiştirme, sayfalama ve filtre değişikliği gibi her "hızlıca değişen sorgu" senaryosunda gerekir; React gibi kütüphanelerde veri kancaları bu işi kendiliğinden yapar.

10Sınav köşesi

Yazılıda nasıl sorulur

(1) JavaScript tek iş parçacıklı mıdır, asenkron işlemler nasıl çalışır? (2) Event loop, call stack, task queue ve microtask queue kavramları — çıktı sırası sorusu sık gelir. (3) Callback hell nedir, nasıl çözülür? (4) Promise'in durumları. (5) then/catch/finally. (6) async/await nedir, Promise ile ilişkisi nedir? (7) Promise.all ile allSettled farkı. (8) fetch ile XMLHttpRequest farkı. (9) fetch hata yönetimi; response.ok neden kontrol edilir? (10) setTimeout(fn, 0) ne yapar? (11) Race condition nedir, nasıl önlenir? (12) CORS hatası ne zaman alınır (1. derse bağlanır)?

Sözlüde nasıl anlatılır — 2 dakikalık taslak

Düzen: (1) Model — "JavaScript tek iş parçacıklıdır; beklemeli işleri çalışma ortamı üstlenir ve olay döngüsü, yığın boşaldığında önce mikro görevleri sonra bir makro görevi çalıştırır. Bu yüzden Promise geri çağrıları setTimeout'tan önce çalışır." (2) Kuşaklar — "geri çağrılar iç içe geçtiği ve hata taşımayı zorlaştırdığı için Promise geldi; async/await ise aynı şeyi senkron gibi okunacak biçimde yazmayı ve try/catch kullanmayı sağlıyor." (3) Kıdem vuruşu — "arka arkaya await yazmak istekleri sıraya sokar; birbirine bağlı değillerse Promise.all ile paralel çalıştırırım. Kısmi sonuç göstereceksem allSettled kullanırım. forEach içinde await beklenmez — for...of ya da Promise.all(map(...)) yazarım." (4) fetch gerçeği — "fetch 404 ve 500'de reddetmez, yalnız ağ hatasında reddeder; bu yüzden yanit.ok kontrolünü elle yazar, durum kodunu taşıyan bir hata sınıfı fırlatırım. Zaman aşımı yerleşik değildir, AbortSignal.timeout eklerim." (5) Üretim davranışı — "aramada yarış koşulunu debounce, iptal ve sıra numarasıyla çözerim; yeniden denemeyi yalnız idempotent isteklerde ve üstel geri çekilmeyle yaparım. Ekranı beş durumuyla sürerim ve yakalanmayan reddetmeleri unhandledrejection ile izleme aracına bağlarım."