← Kursa Dön
📄 Text · 30 min

Asenkron Programlama Nedir?

Neden Bu Konu Önemli?

Bir restorana gittiğini düşün. Garson siparişini aldıktan sonra mutfağa gidiyor, yemeğin pişmesini bekliyor ve sana getiriyor. Ama garson sadece senin masanla mı ilgileniyor? Hayır! Siparişi mutfağa verdikten sonra diğer masalara geçiyor, içecek servisi yapıyor, hesap getiriyor. Yemeğin hazır olduğunda tekrar sana dönüyor.

İşte JavaScript tam olarak bu garson gibi çalışır. Tek bir garson (single thread) olmasına rağmen, aynı anda birçok masaya hizmet verebilir. Çünkü bir işin bitmesini beklemeye gerek olmayan durumlarda, o işi başlatıp diğer işlerle ilgilenmeye devam eder.

Bu derste JavaScript'in bu "sihirli" yeteneğinin arkasındaki mekanizmayı — event loop, call stack, callback queue ve microtask queue — derinlemesine öğreneceksin.


Single-Threaded Ne Demek?

JavaScript tek iş parçacıklı (single-threaded) bir dildir. Bu ne anlama geliyor? Bir anda sadece bir iş yapabilir. İki kod satırı aynı anda, paralel olarak çalışamaz.

Peki bu bir dezavantaj mı? İlk bakışta öyle görünüyor. Java veya C++ gibi dillerde birden fazla thread açabilir, farklı işleri gerçekten paralel çalıştırabilirsin. Ama multi-threading kendi sorunlarını getirir: race condition, deadlock, shared state yönetimi... JavaScript tasarımcıları bu karmaşıklıktan kaçınmak için bilinçli bir tercih yaptılar.

Tek thread olmasına rağmen JavaScript'in yavaş olmadığını, hatta I/O-yoğun (input/output ağırlıklı) işlerde son derece verimli olduğunu göreceğiz. Bunun sırrı asenkron programlama modelinde yatıyor.

// JavaScript tek iş parçacıklı — bu iki satır SIRAYLA çalışır
console.log("Birinci"); // 1. bu çalışır
console.log("İkinci");  // 2. sonra bu çalışır
// Asla aynı anda çalışmazlar

Senkron vs Asenkron: Temel Fark

Senkron (Synchronous) Çalışma

Senkron demek, her işin bir öncekinin bitmesini beklemesi demektir. Bir banka kuyruğu gibi düşün: sıradaki kişi, önündeki kişinin işlemi bitene kadar bekler. Kimse sıra atlayamaz.

// Senkron çalışma — her satır bir öncekinin bitmesini bekler
console.log("1. Veriyi hazırla");
console.log("2. Veriyi işle");      // 1. satır bitmeden buraya gelinmez
console.log("3. Sonucu göster");    // 2. satır bitmeden buraya gelinmez

// Çıktı her zaman:
// 1. Veriyi hazırla
// 2. Veriyi işle
// 3. Sonucu göster

Bu basit durumlar için gayet iyi çalışır. Ama ya bir işlem 5 saniye sürüyorsa? API'den veri çekmek, dosya okumak, veritabanına sorgu göndermek gibi?

// ⚠️ Senkron modelde uzun süren iş her şeyi BLOKLAR
console.log("Başladı");

// Bu 5 saniye boyunca HIÇBIR ŞEY çalışmaz
// Tarayıcıda sayfa donar, butonlar çalışmaz, scroll yapılamaz
const sonuc = bekle5SaniyeVeVeriGetir(); // (varsayımsal senkron fonksiyon)

console.log("Bitti"); // 5 saniye sonra ancak buraya gelir

Asenkron (Asynchronous) Çalışma

Asenkron modelde ise uzun süren işler arka plana alınır. Ana program akışı devam eder. İş tamamlandığında sonuç geri bildirilir.

// Asenkron çalışma — setTimeout ile basit örnek
console.log("1. Başladı");

setTimeout(() => {
  console.log("2. Bu 2 saniye sonra çalışır");
}, 2000);

console.log("3. Bu HEMEN çalışır — beklemez!");

// Çıktı:
// 1. Başladı
// 3. Bu HEMEN çalışır — beklemez!
// (2 saniye sonra)
// 2. Bu 2 saniye sonra çalışır

Dikkat et: 3. satır, 2. satırın bitmesini beklemiyor. setTimeout çağrısı yapıldığında JavaScript "tamam, 2 saniye sonra bu fonksiyonu çalıştır" deyip hemen bir sonraki satıra geçiyor. Bu, non-blocking (engellemesiz) çalışma demek.

💡 İpucu: Asenkron ≠ paralel. JavaScript hâlâ tek thread'de çalışıyor. Sadece uzun süren I/O işlemlerini tarayıcının veya Node.js'in altyapısına devredip, sonuçlarını daha sonra işliyor.


Call Stack: JavaScript'in Beyni

Call stack (çağrı yığını), JavaScript motorunun "şu an hangi fonksiyonu çalıştırıyorum?" sorusunun cevabını tuttuğu veri yapısıdır. LIFO (Last In, First Out — son giren ilk çıkar) prensibiyle çalışır. Bir tabak yığını gibi düşün: en üste koyduğun tabağı ilk alırsın.

function ucuncuFonksiyon() {
  console.log("Üçüncü fonksiyon çalıştı");
  // Burada call stack:
  // 3. ucuncuFonksiyon() ← en üstte, şu an çalışan
  // 2. ikinciFonksiyon()
  // 1. birinciFonksiyon()
}

function ikinciFonksiyon() {
  console.log("İkinci fonksiyon çalıştı");
  ucuncuFonksiyon();
  // ucuncuFonksiyon bittikten sonra buraya dönülür
}

function birinciFonksiyon() {
  console.log("Birinci fonksiyon çalıştı");
  ikinciFonksiyon();
  // ikinciFonksiyon bittikten sonra buraya dönülür
}

birinciFonksiyon();

// Call stack'in zaman çizelgesi:
// 1. birinciFonksiyon() → stack'e eklenir
// 2. ikinciFonksiyon() → stack'e eklenir (birinci'nin üstüne)
// 3. ucuncuFonksiyon() → stack'e eklenir (ikinci'nin üstüne)
// 4. ucuncuFonksiyon() biter → stack'ten çıkar
// 5. ikinciFonksiyon() biter → stack'ten çıkar
// 6. birinciFonksiyon() biter → stack'ten çıkar
// 7. Stack boş — program bitti

Stack Overflow

Call stack'in bir kapasitesi var. Eğer bir fonksiyon kendini sonsuz kez çağırırsa (infinite recursion), stack taşar:

// ⚠️ Bu kod çalıştırılmamalı — stack overflow hatası verir!
function sonsuzdöngü() {
  sonsuzdöngü(); // Kendini çağırır, hiç bitmez
}

sonsuzdöngü();
// Hata: "Maximum call stack size exceeded"
// Tarayıcıya göre 10.000-25.000 çağrı arasında taşar

⚠️ Dikkat: Call stack dolduğunda JavaScript motoru çalışmayı durdurur ve hata fırlatır. Bu yüzden recursive (özyinelemeli) fonksiyonlarda mutlaka bir çıkış koşulu (base case) olmalıdır.


Web API'leri: JavaScript'in Yardımcıları

JavaScript motoru (V8, SpiderMonkey vb.) tek başına sadece senkron kod çalıştırabilir. Peki setTimeout, fetch, addEventListener gibi asenkron fonksiyonlar nereden geliyor?

Bunlar JavaScript dilinin değil, çalışma ortamının (runtime environment) sağladığı API'lerdir:

  • Tarayıcıda: Web API'leri (DOM, setTimeout, fetch, XMLHttpRequest, Geolocation...)

  • Node.js'te: C++ ile yazılmış libuv kütüphanesi (fs, http, timers...)

Bu API'ler, JavaScript motorunun dışında, ayrı thread'lerde çalışır. İşte asenkronluğun sırrı burada: JavaScript tek thread olsa da, çalışma ortamı çoklu thread kullanabilir.

// setTimeout JavaScript'in DEĞİL, Web API'nin bir parçasıdır
console.log("A");

setTimeout(() => {
  console.log("B"); // Bu callback, Web API'ye teslim edilir
}, 1000);

console.log("C");

// Akış:
// 1. "A" → call stack'te çalışır, yazdırılır
// 2. setTimeout → Web API'ye callback teslim edilir
//    (call stack'ten HEMEN çıkar, beklemez)
// 3. "C" → call stack'te çalışır, yazdırılır
// 4. 1 saniye sonra Web API callback'i queue'ya koyar
// 5. Call stack boşalınca callback çalışır → "B" yazdırılır

Bu akışı tam olarak anlamak için sıradaki kavrama geçmemiz lazım: Event Loop.


Event Loop: Orkestra Şefi

Event loop, JavaScript'in asenkron çalışma modelinin kalbidir. Görevi basit ama kritik: call stack'in boş olup olmadığını kontrol etmek ve boşsa kuyruktan (queue) bir sonraki işi alıp stack'e koymak.

Analoji: Restoran Mutfağı

Bunu bir restoran mutfağı olarak hayal et:

  • Call Stack = Şef aşçının elindeki tabak (şu an üzerinde çalıştığı yemek)

  • Web API'ler = Fırın, buzdolabı, ızgara (yemeği pişiren cihazlar)

  • Callback Queue = Hazır yemeklerin konduğu tezgah

  • Event Loop = Şefin "Elimdeki tabak bitti mi? Tezgahta hazır yemek var mı?" diye kontrol etmesi

Şef aşçı (call stack) bir anda sadece bir tabakla ilgilenir. Ama fırına (Web API) bir şey koyduğunda, fırın kendi kendine pişirir. Pişince tezgaha (queue) konur. Şefin eli boşalınca tezgahtan alır.

Somut Örnek

console.log("1");

setTimeout(() => {
  console.log("2");
}, 0); // 0 milisaniye bile olsa!

console.log("3");

// Çıktı:
// 1
// 3
// 2  ← 0ms olmasına rağmen en sona!

Bu çıktı çoğu yeni başlayanı şaşırtır. 0 milisaniye bekle dedik, neden "2" en sona düştü? Çünkü setTimeout — süre 0 bile olsa — callback'i Web API'ye verir, oradan queue'ya geçer ve ancak call stack tamamen boşaldığında çalıştırılır.

Event Loop'un Çalışma Döngüsü

┌──────────────────────────────────────┐
│           Call Stack                 │
│    (Şu an çalışan fonksiyon)         │
└──────────────┬───────────────────────┘
               │
               │ Stack boş mu?
               │
    ┌──────────▼──────────┐
    │     Event Loop       │
    │  (Sürekli kontrol)   │
    └──────────┬──────────┘
               │
     ┌─────────┴─────────┐
     │                    │
┌────▼─────┐    ┌────────▼────────┐
│Microtask │    │  Callback Queue  │
│  Queue   │    │  (Task Queue)    │
│(Öncelik!)│    │                  │
└──────────┘    └─────────────────┘

Event loop her döngüde (tick) şunu yapar:

  1. Call stack'i kontrol et

  2. Stack boşsa, önce microtask queue'dan bir iş al

  3. Microtask queue boşsa, callback queue'dan (macrotask queue) bir iş al

  4. Alınan işi call stack'e koy ve çalıştır

  5. Başa dön


Callback Queue (Macrotask Queue)

Callback queue, Web API'lerin tamamlanan işlerinin geri çağırma fonksiyonlarını (callback) beklediği kuyruktur. FIFO (First In, First Out — ilk giren ilk çıkar) prensibiyle çalışır.

Hangi işler macrotask queue'ya gider?

  • setTimeout / setInterval callback'leri

  • DOM event handler'ları (click, scroll vb.)

  • requestAnimationFrame (teknik olarak ayrı bir queue ama konsept aynı)

  • I/O operasyonları (Node.js'te)

console.log("Başla");

setTimeout(() => {
  console.log("Timeout 1");
}, 0);

setTimeout(() => {
  console.log("Timeout 2");
}, 0);

console.log("Bitir");

// Çıktı:
// Başla
// Bitir
// Timeout 1  ← FIFO: ilk eklenen ilk çalışır
// Timeout 2

Microtask Queue: VIP Kuyruğu

Microtask queue, callback queue'dan daha öncelikli bir kuyruktur. Event loop her tick'te önce microtask queue'yu tamamen boşaltır, ancak ondan sonra callback queue'ya bakar.

Hangi işler microtask queue'ya gider?

  • Promise callback'leri (.then(), .catch(), .finally())

  • queueMicrotask() ile eklenen görevler

  • MutationObserver callback'leri

Bu ayrım neden önemli? Çünkü bir Promise'in sonucu, bir setTimeout'tan her zaman önce işlenir — setTimeout 0ms bile olsa.

console.log("1 - Senkron");

setTimeout(() => {
  console.log("2 - Macrotask (setTimeout)");
}, 0);

Promise.resolve().then(() => {
  console.log("3 - Microtask (Promise)");
});

console.log("4 - Senkron");

// Çıktı:
// 1 - Senkron
// 4 - Senkron
// 3 - Microtask (Promise) ← setTimeout'tan ÖNCE!
// 2 - Macrotask (setTimeout)

Neden İki Ayrı Kuyruk?

Microtask queue'nun amacı, mevcut görevin devamı niteliğindeki işlerin hemen sonra çalışmasını garanti etmektir. Bir Promise chain'i düşün: .then().then().then() — bunların araya başka iş girmeden, art arda çalışması gerekir. Eğer bunlar macrotask queue'da olsaydı, araya DOM event'leri, setTimeout'lar girebilir ve Promise chain'inin tutarlılığı bozulurdu.

// Microtask queue tamamen boşaltılır, sonra macrotask'a geçilir
console.log("Start");

setTimeout(() => console.log("Macro 1"), 0);
setTimeout(() => console.log("Macro 2"), 0);

Promise.resolve()
  .then(() => {
    console.log("Micro 1");
    // Microtask içinden yeni microtask ekleyebiliriz
    Promise.resolve().then(() => console.log("Micro 3"));
  })
  .then(() => console.log("Micro 2"));

console.log("End");

// Çıktı:
// Start
// End
// Micro 1
// Micro 2  ← Micro 3'ten önce (chain sırası)
// Micro 3  ← Hâlâ macrotask'tan önce!
// Macro 1
// Macro 2

💡 İpucu: Microtask queue içinden sürekli yeni microtask eklerseniz, macrotask queue'ya asla sıra gelmez. Bu, UI'ın donmasına (starvation) neden olabilir. Dikkatli kullanın.


Callback Pattern: Her Şeyin Başladığı Yer

Asenkron JavaScript'in ilk çözümü callback (geri çağırma) fonksiyonlarıydı. Bir işlem tamamlandığında çağrılacak fonksiyonu parametre olarak geçersin.

// Basit callback örneği
function veriGetir(url, basariliCallback, hataCallback) {
  // Simülasyon: rastgele başarı veya hata
  setTimeout(() => {
    const basarili = Math.random() > 0.3;
    
    if (basarili) {
      basariliCallback({ id: 1, name: "Ahmet" });
    } else {
      hataCallback(new Error("Veri getirilemedi"));
    }
  }, 1000);
}

// Kullanım
veriGetir(
  "/api/users/1",
  (data) => console.log("Veri geldi:", data),
  (err) => console.error("Hata:", err.message)
);

Bu basit durumlar için işe yarar. Ama ya birbirine bağımlı birden fazla asenkron işlem yapman gerekirse?

Callback Hell (Cehennem Piramidi)

Birbirine bağımlı asenkron işlemler iç içe callback'lerle yazıldığında, kod sağa doğru kayarak okunaksız hale gelir. Bu duruma "Callback Hell" veya "Pyramid of Doom" denir.

// ⚠️ Callback Hell — gerçek dünya örneği
kullaniciyiGetir(userId, (kullanici) => {
  siparisleriGetir(kullanici.id, (siparisler) => {
    sonSiparisiGetir(siparisler[0].id, (siparis) => {
      urunDetayGetir(siparis.urunId, (urun) => {
        yorumlariGetir(urun.id, (yorumlar) => {
          console.log("Yorumlar:", yorumlar);
          // 5 seviye iç içe... ve hata yönetimi bile yapmadık!
        }, (err) => console.error(err));
      }, (err) => console.error(err));
    }, (err) => console.error(err));
  }, (err) => console.error(err));
}, (err) => console.error(err));

Bu kod:

  • Okunamaz — sağa doğru kayan piramit yapısı

  • Hata yönetimi zor — her seviyede ayrı hata callback'i

  • Bakımı imkânsız — yeni bir adım eklemek veya sırayı değiştirmek kabus

  • Test edilemez — her şey iç içe geçmiş

Bu sorun o kadar yaygındı ki, JavaScript topluluğu daha iyi çözümler aradı. Ve Promise doğdu. (Promise'leri bir sonraki derste derinlemesine öğreneceğiz.)


Blocking vs Non-Blocking: Pratik Fark

Bu iki kavramı somut bir örnekle görelim. Bir web sayfasında ağır bir hesaplama yapıyorsun:

// ⚠️ BLOCKING — Sayfayı donduran kod
function agirHesaplama() {
  let toplam = 0;
  for (let i = 0; i < 5_000_000_000; i++) { // 5 milyar iterasyon
    toplam += i;
  }
  return toplam;
}

console.log("Hesaplama başlıyor...");
const sonuc = agirHesaplama(); // Bu satır bitene kadar SAYFA DONAR
console.log("Sonuç:", sonuc);
// Kullanıcı bu sürede butona tıklayamaz, scroll yapamaz
// Tarayıcı "Bu sayfa yanıt vermiyor" uyarısı gösterebilir
// ✅ NON-BLOCKING — Ağır işi parçalara bölen yaklaşım
function agirHesaplamaAsync(callback) {
  let toplam = 0;
  let i = 0;
  const PARCA_BOYUTU = 1_000_000;
  
  function parcaCalistir() {
    const bitis = Math.min(i + PARCA_BOYUTU, 5_000_000_000);
    
    while (i < bitis) {
      toplam += i;
      i++;
    }
    
    if (i < 5_000_000_000) {
      // Bir sonraki parçayı event loop'a bırak
      // Bu sayede araya UI güncellemeleri girebilir
      setTimeout(parcaCalistir, 0);
    } else {
      callback(toplam);
    }
  }
  
  parcaCalistir();
}

console.log("Hesaplama başlıyor...");
agirHesaplamaAsync((sonuc) => {
  console.log("Sonuç:", sonuc);
});
console.log("Bu hemen çalışır — sayfa donmaz!");

İkinci yaklaşımda işi küçük parçalara bölüp, her parça arasında event loop'a kontrolü veriyoruz. Böylece tarayıcı UI güncellemelerini yapabilir, kullanıcı etkileşimleri çalışabilir.


setTimeout ve setInterval'ı Doğru Anlamak

Bu iki fonksiyon en temel asenkron araçlardandır ama çoğu kişi tarafından yanlış anlaşılır.

setTimeout: "En az X ms sonra çalıştır"

// setTimeout garantisi: "TAM x ms sonra" DEĞİL,
// "EN AZ x ms sonra, call stack boşalınca" çalışır

console.log("A");

setTimeout(() => {
  console.log("B — 'En az 100ms sonra'");
}, 100);

// Bu döngü 500ms sürüyorsa, B ancak 500ms sonra çalışır
// Çünkü call stack'in boşalması gerekir
for (let i = 0; i < 1_000_000_000; i++) {
  // Ağır senkron iş — call stack'i meşgul tutar
}

console.log("C");

// Çıktı:
// A
// C  (500ms süren döngü biter)
// B  (100ms DEĞİL, ~500ms sonra! Çünkü stack meşguldü)

⚠️ Dikkat: setTimeout(fn, 100) demek "100ms sonra çalıştır" değil, "100ms sonra kuyruğa ekle, stack boşalınca çalıştır" demektir. Stack meşgulse gecikme yaşanır.

setInterval: Tekrarlayan Zamanlayıcı

// setInterval her X ms'de bir callback'i kuyruğa ekler
let sayac = 0;

const intervalId = setInterval(() => {
  sayac++;
  console.log(`Tick ${sayac}`);
  
  if (sayac >= 5) {
    clearInterval(intervalId); // Durdurmayı unutma!
    console.log("Interval temizlendi");
  }
}, 1000);

// Çıktı (her saniye):
// Tick 1
// Tick 2
// Tick 3
// Tick 4
// Tick 5
// Interval temizlendi

⚠️ Dikkat: setInterval temizlenmezse (clearInterval) sonsuza kadar çalışır ve memory leak'e neden olur. Özellikle SPA (Single Page Application) uygulamalarında component unmount olduğunda interval'ları temizlemeyi unutmayın.

setTimeout ile setInterval Arasındaki Fark

// setInterval: Callback ne kadar sürerse sürsün, X ms'de bir tetikler
// Bu, callback'lerin üst üste binmesine neden olabilir

// Daha güvenli alternatif: recursive setTimeout
function guvenliInterval(fn, ms) {
  function tekrarla() {
    fn();
    setTimeout(tekrarla, ms); // Önceki çalışma BİTTİKTEN sonra bekle
  }
  setTimeout(tekrarla, ms);
}

// Bu yöntemde her çalışma bir öncekinin BİTMESİNİ bekler
// setInterval'da ise callback hâlâ çalışırken yeni bir tetikleme olabilir
guvenliInterval(() => {
  console.log("Bu güvenli bir tekrarlama");
}, 1000);

Event Loop'u Canlı Görelim: Kapsamlı Örnek

Şimdi tüm bu kavramları birleştiren karmaşık bir örneğe bakalım. Her satırın ne zaman, neden çalıştığını adım adım takip edeceğiz:

console.log("1 - Senkron: Script başladı");

setTimeout(() => {
  console.log("2 - Macrotask: setTimeout 0ms");
}, 0);

setTimeout(() => {
  console.log("3 - Macrotask: setTimeout 0ms (ikinci)");
}, 0);

Promise.resolve()
  .then(() => {
    console.log("4 - Microtask: Promise 1");
  })
  .then(() => {
    console.log("5 - Microtask: Promise 1 chain");
  });

Promise.resolve()
  .then(() => {
    console.log("6 - Microtask: Promise 2");
    setTimeout(() => {
      console.log("7 - Macrotask: setTimeout (microtask içinden)");
    }, 0);
  });

queueMicrotask(() => {
  console.log("8 - Microtask: queueMicrotask");
});

console.log("9 - Senkron: Script bitti");

Bu kodun çıktısını tahmin edebilir misin? Adım adım gidelim:

Adım 1 — Senkron kodlar çalışır:

  • "1 - Senkron: Script başladı" yazdırılır

  • setTimeout #1 → Web API'ye gider

  • setTimeout #2 → Web API'ye gider

  • Promise #1 → microtask queue'ya .then eklenir

  • Promise #2 → microtask queue'ya .then eklenir

  • queueMicrotask → microtask queue'ya eklenir

  • "9 - Senkron: Script bitti" yazdırılır

Adım 2 — Microtask queue boşaltılır (sırayla):

  • "4 - Microtask: Promise 1" → chain devam eder, .then eklenir

  • "6 - Microtask: Promise 2" → içindeki setTimeout Web API'ye gider

  • "8 - Microtask: queueMicrotask"

  • "5 - Microtask: Promise 1 chain" (4'ün chain'i)

Adım 3 — Macrotask queue'dan birer birer alınır:

  • "2 - Macrotask: setTimeout 0ms"

  • "3 - Macrotask: setTimeout 0ms (ikinci)"

  • "7 - Macrotask: setTimeout (microtask içinden)"

Tam çıktı:

1 - Senkron: Script başladı
9 - Senkron: Script bitti
4 - Microtask: Promise 1
6 - Microtask: Promise 2
8 - Microtask: queueMicrotask
5 - Microtask: Promise 1 chain
2 - Macrotask: setTimeout 0ms
3 - Macrotask: setTimeout 0ms (ikinci)
7 - Macrotask: setTimeout (microtask içinden)

Node.js'te Event Loop Farkları

Node.js'te event loop, tarayıcıdakine benzer ama ek fazları (phases) vardır:

   ┌───────────────────────────┐
┌─>│         timers            │ ← setTimeout, setInterval callback'leri
│  └──────────┬────────────────┘
│  ┌──────────▼────────────────┐
│  │     pending callbacks     │ ← Bazı sistem operasyonları
│  └──────────┬────────────────┘
│  ┌──────────▼────────────────┐
│  │        idle, prepare      │ ← Dahili kullanım
│  └──────────┬────────────────┘
│  ┌──────────▼────────────────┐
│  │          poll             │ ← I/O callback'leri (dosya, ağ)
│  └──────────┬────────────────┘
│  ┌──────────▼────────────────┐
│  │          check            │ ← setImmediate callback'leri
│  └──────────┬────────────────┘
│  ┌──────────▼────────────────┐
│  │     close callbacks       │ ← socket.on('close', ...)
│  └──────────┴────────────────┘
│             │
└─────────────┘ (döngü devam eder)

Node.js'e özel iki fonksiyon:

// process.nextTick — microtask queue'dan bile ÖNCE çalışır
// (Node.js'e özel, tarayıcıda yok)
process.nextTick(() => {
  console.log("nextTick — en yüksek öncelik");
});

Promise.resolve().then(() => {
  console.log("Promise — microtask");
});

setTimeout(() => {
  console.log("setTimeout — macrotask");
}, 0);

// setImmediate — poll fazından sonra çalışır
// (Node.js'e özel, tarayıcıda yok)
setImmediate(() => {
  console.log("setImmediate — check fazı");
});

// Node.js çıktısı:
// nextTick — en yüksek öncelik
// Promise — microtask
// setTimeout — macrotask (veya setImmediate ile sırası değişebilir)
// setImmediate — check fazı (veya setTimeout ile sırası değişebilir)

💡 İpucu: setTimeout(fn, 0) vs setImmediate(fn) sırası, Node.js'te bulundukları context'e göre değişebilir. Ama bir I/O callback'i içinde setImmediate her zaman setTimeout'tan önce çalışır.


Yaygın Hatalar ve Tuzaklar

Hata 1: setTimeout'un Kesin Zamanlama Sağladığını Sanmak

// ❌ Yanlış düşünce: "Tam 1 saniye sonra çalışacak"
setTimeout(() => {
  console.log("1 saniye sonra... belki");
}, 1000);

// ✅ Doğrusu: "EN AZ 1 saniye sonra, stack boşalınca çalışacak"
// Eğer call stack ağır bir işle meşgulse, gecikme olur

Hata 2: for Döngüsünde var ile setTimeout

// ❌ Klasik tuzak
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // Hep 3 yazdırır!
  }, 1000);
}
// Çıktı: 3, 3, 3
// Neden? var function-scoped, döngü bittiğinde i = 3
// setTimeout çalıştığında hepsi aynı i'ye referans veriyor

// ✅ Çözüm 1: let kullan (block-scoped)
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}
// Çıktı: 0, 1, 2

// ✅ Çözüm 2: IIFE ile closure oluştur
for (var i = 0; i < 3; i++) {
  ((j) => {
    setTimeout(() => {
      console.log(j);
    }, 1000);
  })(i);
}
// Çıktı: 0, 1, 2

Hata 3: Microtask Starvation

// ❌ Tehlikeli: Sonsuz microtask — UI donması
function tehlikeli() {
  Promise.resolve().then(() => {
    tehlikeli(); // Sürekli yeni microtask ekler
    // Macrotask queue'ya ASLA sıra gelmez
    // Tarayıcı donar!
  });
}
// tehlikeli(); // ÇALIŞTIRMAYIN!

// ✅ Güvenli alternatif: setTimeout ile macrotask'a geç
function guvenli() {
  setTimeout(() => {
    // İşini yap
    guvenli(); // Macrotask olarak eklenir
    // Araya render ve diğer işler girebilir
  }, 0);
}

Gerçek Dünya Senaryosu: Asenkron İşlemlerin Koordinasyonu

Bir e-ticaret uygulamasında kullanıcı sayfayı açtığında birden fazla veri çekmemiz gerekiyor. Bu işlemlerin nasıl sıralandığını anlamak event loop bilgisiyle doğrudan ilişkili:

// Gerçek dünya senaryosu: Sayfa yükleme
console.log("📦 Sayfa yükleniyor...");

// Senkron: Kritik UI bileşenleri hemen render edilir
console.log("🎨 İskelet (skeleton) gösterildi");

// Asenkron: Veriler paralel olarak çekilir
setTimeout(() => {
  console.log("📊 Analytics script yüklendi");
}, 0);

// Microtask: Önemli veriler önce gelir
Promise.resolve().then(() => {
  console.log("👤 Kullanıcı bilgisi alındı");
}).then(() => {
  console.log("🛒 Sepet bilgisi güncellendi");
});

Promise.resolve().then(() => {
  console.log("📋 Ürün listesi yüklendi");
});

// Daha düşük öncelikli
setTimeout(() => {
  console.log("💬 Canlı destek widget'ı yüklendi");
}, 100);

setTimeout(() => {
  console.log("🍪 Cookie banner gösterildi");
}, 0);

console.log("✅ İlk render tamamlandı");

// Çıktı sırası:
// 📦 Sayfa yükleniyor...
// 🎨 İskelet (skeleton) gösterildi
// ✅ İlk render tamamlandı
// 👤 Kullanıcı bilgisi alındı       (microtask)
// 📋 Ürün listesi yüklendi          (microtask)
// 🛒 Sepet bilgisi güncellendi      (microtask chain)
// 📊 Analytics script yüklendi      (macrotask - 0ms)
// 🍪 Cookie banner gösterildi       (macrotask - 0ms)
// 💬 Canlı destek widget'ı yüklendi (macrotask - 100ms)

Bu sıralama tesadüf değil — bilinçli bir mimari karar. Kullanıcı bilgisi ve sepet (microtask) analytics'ten (macrotask) önce gelir çünkü kullanıcı deneyimi için kritiktir.


Özet

Bu derste JavaScript'in asenkron çalışma modelinin temellerini öğrendik:

  • Single-threaded olmasına rağmen JavaScript non-blocking çalışabilir çünkü uzun süren işleri çalışma ortamına (Web API / libuv) devreder.

  • Call stack şu an çalışan fonksiyonları LIFO düzeninde tutar. Taşarsa "stack overflow" hatası alırız.

  • Event loop sürekli call stack'i kontrol eder. Stack boşaldığında kuyruktan iş alır.

  • Microtask queue (Promise, queueMicrotask) macrotask queue'dan (setTimeout, setInterval) her zaman önceliklidir.

  • Callback pattern asenkron programlamanın ilk çözümüydü ama "callback hell" sorununa yol açtı.

  • setTimeout(fn, 0) "hemen çalıştır" demek değildir — "stack boşalınca en kısa sürede çalıştır" demektir.

Bir sonraki derste, callback hell'in çözümü olarak doğan Promise'leri öğreneceğiz.