Önceki yazımızda JavaScript motorunun (V8 vb.) Call Stack üzerinde senkron kodları nasıl çalıştırdığını inceledik. JavaScript'in doğası gereği Single-Thread (Tek İş Parçacıklı) olduğunu, yani aynı anda sadece tek bir fonksiyonu çalıştırabildiğini biliyoruz.
Peki, uzun süren bir veritabanı sorgusu, bir dosya okuma veya sunucudan veri çekme (network HTTP isteği) işlemi yapmamız gerektiğinde ne olur?
Eğer bu işlemleri senkron olarak Call Stack üzerinde çalıştırsaydık, yanıt gelene kadar tüm uygulama donar ve kullanıcı hiçbir butonla etkileşime giremezdi (Main Thread Bloklanması).
İşte JavaScript ekosistemi, arka planda çalışan Event Loop ve Web APIs mimarisi sayesinde bu tıkama problemini çözdü. Biz yazılımcıların bu asenkron mekanizmayı kod tarafında yönetebilmesi için ise tarihsel süreçte farklı yapı ve sözdizimleri gelişti. Bu serüvenin ilk ve en temel yapı taşı: Callback Fonksiyonlardır.
1. Callback (Geri Çağırma) Nedir?
En basit ve yalın tanımıyla Callback; başka bir fonksiyona parametre olarak geçirilen ve o ana fonksiyon içindeki iş tamamlandığında çalıştırılmak (geri çağrılmak) üzere bekletilen fonksiyondur.
JavaScript’te fonksiyonlar First-Class Citizens (Birinci Sınıf Vatandaş) kabul edilir. Yani bir fonksiyonu bir değişkene atayabilir, başka bir fonksiyona parametre olarak gönderebilir veya bir fonksiyondan değer olarak döndürebilirsiniz. Callback mekanizması tamamen bu esnekliğe dayanır.
Kod Üzerinden Callback Mantığı
// Callback alan asenkron simülasyon fonksiyonu
function veriyiGetir(callback) {
console.log("1. Veri sunucudan isteniyor...");
// 2 saniye süren asenkron bir işlem simülasyonu
setTimeout(() => {
const veri = { id: 1, name: "Ahmet" };
console.log("2. Veri sunucudan geldi!");
// İş bitti! Bize teslim edilen callback fonksiyonunu çağırıyoruz:
callback(veri);
}, 2000);
}
// Callback fonksiyonumuz
function veriyiIsle(gelenVeri) {
console.log("3. Gelen veri işleniyor:", gelenVeri.name);
}
// Fonksiyonu çalıştırıyoruz ve 'veriyiIsle' fonksiyonunu parametre olarak veriyoruz
veriyiGetir(veriyiIsle);Konsol Çıktısı Sıralaması:
1. Veri sunucudan isteniyor...(2 saniyelik bekleme süresince Call Stack boşalır, sayfa kilitlenmez)
2. Veri sunucudan geldi!3. Gelen veri işleniyor: Ahmet
1.1 Waterfall Execution (Şelale Tipi Yürütme) ve Callback Hell (Anti-Pattern)
Uygulamalarımız büyüdükçe asenkron işlemler genellikle birbirine bağımlı hale gelir. Bir işlemin çıktısı, bir sonraki işlemin girdisi olmak zorundadır.
Örneğin bir e-ticaret sistemindeki şu adım adım akışı düşünün:
Kullanıcı ID'si ile kullanıcı detaylarını çek.
Kullanıcının son siparişlerini çek (1. adımın bitmesini beklemek zorunda).
Siparişteki ürünlerin stok durumunu kontrol et (2. adımın bitmesini beklemek zorunda).
Ödeme faturasını oluştur (3. adımın bitmesini beklemek zorunda).
Asenkron kodların bir şelalenin basamakları gibi yukarıdan aşağıya, birbirinin bitişine bağlı olarak sırayla yürütülmesine Waterfall Execution (Şelale Tipi Yürütme) denir.
Problem: Callback Hell (Piramit Yapısı)
Yazılımcılar bu Waterfall akışını sadece Callback'ler kullanarak çözmeye çalıştıklarında, fonksiyonlar iç içe girer ve kod sağa doğru uzamaya başlar. Yazılım dünyasında buna Callback Hell (Callback Cehennemi) veya kodun şeklinden dolayı Pyramid of Doom (Kader Piramidi) denir.
// 🔴 Callback Hell (Okunması ve Yönetilmesi İmkânsız Yapı)
kullaniciGetir(1, function(kullanici) {
siparisleriGetir(kullanici.id, function(siparisler) {
stokKontrolEt(siparisler[0].id, function(stokDurumu) {
faturaOlustur(stokDurumu, function(fatura) {
mailGonder(fatura, function(sonuc) {
console.log("İşlem başarıyla tamamlandı!");
}, hataYakala);
}, hataYakala);
}, hataYakala);
}, hataYakala);
});Callback Hell Neden Bir Problemdir?
Okunabilirlik Yerlerde: Kod yukarıdan aşağıya değil, çapraz/sağa doğru girintilenir. Mantıksal takibi imkânsız hale gelir.
Hata Yönetimi (Error Handling) Kabusu: Her iç içe fonksiyonda ayrı ayrı hata kontrolleri yazmak gerekir. Tek bir noktada hata oluştuğunda tüm zincir kırılır ve hatanın nereden geldiğini bulmak zorlaşır.
Inversion of Control (Kontrolün Kaybı): Kendi yazdığınız Callback fonksiyonunun yönetimini, çağırdığınız başka bir fonksiyona (veya 3rd-party bir kütüphaneye) devredersiniz. O kütüphanenin Callback'inizi 2 kez çağırıp çağırmayacağından veya hiç çağırmayacağından emin olamazsınız.
2. Promise Yapısı ve Metodları
Callback Hell'in getirdiği okunabilirlik zorluğu ve kontrol kaybı (Inversion of Control), JavaScript ekosisteminde asenkron kodları daha güvenli ve doğrusal yönetebilmek adına yeni bir yapının doğmasını zorunlu kıldı: Promise (Söz). (Daha önce Bluebird veya Q gibi topluluk kütüphaneleriyle kullanılan Promise mimarisi, ES6 / ECMAScript 2015 ile birlikte resmi olarak JavaScript standardına dahil edildi.)
Promise, esasta gelecekte tamamlanacak (veya başarısız olacak) asenkron bir işlemin nihai sonucunu temsil eden bir JavaScript nesnesidir (Object).
Callback mantığında siz fonksiyonunuzu başkasına teslim edersiniz (Inversion of Control). Promise mimarisinde ise asenkron işlem size bir "söz nesnesi" döndürür ve siz bu nesne üzerinden süreci tamamen kendi kontrolünüzde yönetirsiniz.
2.1 Promise’in Yaşam Döngüsü ve 3 Durumu (States)
Bir Promise nesnesi oluşturulduğu andan itibaren 3 temel durumdan (State) birinde bulunmak zorundadır. Bu durumlar arasındaki geçişler tek yönlüdür (immutable); yani sonuçlanan bir Promise tekrar başlangıç durumuna dönemez.
Pending(Beklemede): Asenkron işlemin henüz tamamlanmadığı, başlangıç durumudur. Değer henüz belirsizdir.Fulfilled(Yerine Getirildi / Başarılı): Asenkron işlemin başarıyla sonuçlandığı durumdur. İşlem bir değer (value) döndürür ve.then()bloğu tetiklenir.Rejected(Reddedildi / Hatalı): Asenkron işlemin bir hatayla karşılaştığı veya başarısız olduğu durumdur. Bir hata sebebi (reason/error) döndürür ve.catch()bloğu tetiklenir.
Not: Bir Promise Fulfilled veya Rejected durumuna ulaştığında artık Settled (Karara Bağlanmış) kabul edilir.
2.2 Promise Zincirleme (Promise Chaining)
Promise yapısının Callback Hell'i nasıl çözdüğünü görmek için aynı Waterfall (şelale) akışını Promise zinciri ile yazalım.
Promise nesneleri üzerindeki .then() metodu varsayılan olarak yeni bir Promise döndürür. Bu sayede asenkron işlemleri sağa doğru girintilemek yerine, yukarıdan aşağıya doğrusal (flat) bir hat üzerinde birbirine bağlayabiliriz.
// Promise tabanlı asenkron akış
kullaniciGetir(1)
.then(kullanici => siparisleriGetir(kullanici.id))
.then(siparisler => stokKontrolEt(siparisler[0].id))
.then(stokDurumu => faturaOlustur(stokDurumu))
.then(fatura => mailGonder(fatura))
.then(sonuc => {
console.log("Tüm işlemler sırayla ve başarıyla tamamlandı!");
})
.catch(hata => {
// ZİNCİRDEKİ HERHANGİ BİR ADIMDA HATA OLUŞURSA
// Doğrudan buraya düşer. Tek bir merkezden hata yönetimi!
console.error("İşlem adımlarından birinde hata oluştu:", hata);
})
.finally(() => {
// Başarılı da olsa hatalı da olsa EN SON mutlaka çalışır.
console.log("İşlem akışı kapandı, temizlik yapılıyor.");
});2.3 İleri Seviye Promise Metodları (Paralel Yönetim)

Her asenkron işlem birbirini beklemek zorunda değildir. Bazen birden fazla bağımsız isteği aynı anda (paralel) başlatıp sonuçlarını tek bir noktada toplamak isteriz. JavaScript bu senaryolar için statik Promise metodları sunar:
1. Promise.all([promise1, promise2, ...])
Tüm Promise'lerin başarıyla tamamlanmasını bekler.
Çalışma Mantığı: "Ya hepsi ya hiç!" mantığıdır. Dizi içindeki tüm işlemler
Fulfilledolursa hepsinin sonucunu bir dizi olarak döndürür.Kritik Detay: Eğer tek bir Promise dahi
Rejectedolursa, diğerlerinin bitmesini beklemeden ANINDA hataya düşer (Fail-Fast).
const p1 = fetch('/api/kullanici');
const p2 = fetch('/api/urunler');
Promise.all([p1, p2])
.then(([kullaniciRes, urunlerRes]) => {
console.log("İki istek de başarıyla tamamlandı.");
})
.catch(hata => console.error("İsteklerden EN AZ BİRİ başarısız oldu!", hata));2. Promise.allSettled([promise1, promise2, ...])
Promise.all'un aksine, işlemlerin başarılı ya da başarısız olmasıyla ilgilenmez.
Çalışma Mantığı: Verilen tüm Promise'lerin sonuçlanmasını (
Settledolmasını) bekler.Çıktı: Hangi işlemin başarılı (
fulfilled), hangisinin hatalı (rejected) olduğunu ve döndürdükleri değerleri bir rapor dizisi halinde teslim eder. Bir hatada durmaz.
3. Promise.race([promise1, promise2, ...])
Girdi olarak verilen Promise'lerden en hızlı biteni esas alır.
Çalışma Mantığı: Yarışı ilk kazanan Promise (
FulfilledveyaRejected) neyse, sonucunu veya hatasını doğrudan döndürür. Diğer Promise'lerin sonuçları göz ardı edilir. (Örn: Timeout/Zaman aşımı mekanizmaları kurmak için sıklıkla kullanılır).
4. Promise.any([promise1, promise2, ...])
Dizi içindeki İLK BAŞARILI (Fulfilled) Promise'i yakalar.
Çalışma Mantığı: Hataları görmezden gelir, ilk başaranın değerini döndürür. Yalnızca dizideki tüm Promise'ler başarısız olursa hataya düşer (
AggregateError).
Promise mimarisi asenkron kod yönetimini Callback Hell kabusundan kurtarmış olsa da, zincirleme .then() blokları karmaşık projelerde hâlâ senkron kod kadar temiz bir okuma deneyimi sunmuyordu. İşte bu noktada, ES2017 (ES8) ile birlikte sahneye asenkron kod yazımında çığır açan Async / Await çıktı.
3. Async / Await ve Modern Hata Yönetimi
Asenkron JavaScript evriminde Callback'ler "temel taş", Promise'ler "düzenleyici yapı" ise, Async / Await bu sürecin zirve noktasıdır.
Günümüz modern JavaScript dünyasında asenkron kod yazmanın standart yolu haline gelen bu yaklaşım, bizi callback karmaşasından ve uzun .then() zincirlerinden tamamen kurtarır.
3.1 Async / Await Nedir? (Syntactic Sugar Mantığı)
async/await, arka planda tamamen Promise nesnelerini ve Generator fonksiyon mantığını kullanır. Aradaki tek fark, yazılımcıyı .then() ve .catch() callback zincirlerinden kurtarmasıdır.
asyncAnahtar Kelimesi: Bir fonksiyonun başınaasynceklendiğinde, o fonksiyon artık otomatik olarak her zaman bir Promise döndürür. Fonksiyon içinden düz bir değer dönseniz bile (örneğinreturn 42;), JavaScript bunu arka plandaPromise.resolve(42)ile sarar.awaitAnahtar Kelimesi: Sadeceasyncfonksiyonlar içinde kullanılabilir. Bir Promise'in önüneawaitkonulduğunda, JavaScript motoru o Promise sonuçlanana (FulfilledveyaRejectedolana) kadar o fonksiyonun yürütülmesini duraklatır.
⚠️
awaitsatırında fonksiyonun duraklaması, tüm tarayıcının veya Main Thread'in donduğu anlamına gelmez! JavaScript motoru o async fonksiyonu kenara alıp bekletirken, Call Stack üzerindeki diğer senkron işleri ve kullanıcı etkileşimlerini çalıştırmaya devam eder.
3.2 Kod Evrimi: Callback -> Promise -> Async/Await
Aynı Waterfall (şelale) senaryomuzu üç farklı dönemle kıyaslayalım. Farkın ne kadar devasa olduğunu net bir şekilde göreceksiniz:
A. Dönem: Callback
kullaniciGetir(1, function(kullanici) {
siparisleriGetir(kullanici.id, function(siparisler) {
faturaOlustur(siparisler[0].id, function(fatura) {
console.log(fatura);
});
});
});B. Dönem: Promise Chaining
kullaniciGetir(1)
.then(kullanici => siparisleriGetir(kullanici.id))
.then(siparisler => faturaOlustur(siparisler[0].id))
.then(fatura => console.log(fatura))
.catch(hata => console.error(hata));C. Dönem: Async / Await
async function siparisSureciniYonet() {
// Kod yukarıdan aşağıya senkron gibi akar ama tamamen asenkrondur!
const kullanici = await kullaniciGetir(1);
const siparisler = await siparisleriGetir(kullanici.id);
const fatura = await faturaOlustur(siparisler[0].id);
console.log(fatura);
}3.3 Modern Hata Yönetimi: try / catch / finally
Promise zincirlerinde hataları .catch() ile yakalıyorduk. Async/Await mimarisinde ise geleneksel ve alışkın olduğumuz senkron hata yönetimi bloğu olan try...catch kullanılır.
async function güvenliSiparisYonetimi() {
try {
const kullanici = await kullaniciGetir(1);
const siparisler = await siparisleriGetir(kullanici.id);
const fatura = await faturaOlustur(siparisler[0].id);
return fatura;
} catch (hata) {
// Yukarıdaki await işlemlerinin HERHANGİ BİRİNDE Promise 'Reject' olursa
// Akış anında durur ve kod buraya sıçrar!
console.error("Süreç esnasında hata oluştu:", hata.message);
} finally {
// Hata olsun ya da olmasın her durumda çalışır (Örn: Loader kapatma)
console.log("İşlem sonlandı.");
}
}3.4 Seri mi, Paralel mi?
async/await kullanırken yazılımcıların en sık düştüğü tuzak, birbirine bağımlı olmayan asenkron işlemleri gereksiz yere peş peşe await ederek uygulamayı yavaşlatmaktır (Sequential Execution Trap).
❌ Hatalı (Yavaş) Yaklaşım:
async function verileriGetirYavas() {
// Bu iki istek birbirinden bağımsız!
// Ancak 2. istek, 1. isteğin bitmesini BOŞ YERE bekler.
const kullanici = await fetch('/api/kullanici'); // 2 saniye
const urunler = await fetch('/api/urunler'); // 2 saniye
// Toplam Süre: ~4 saniye!
}✅ Doğru (Paralel / Hızlı) Yaklaşım:
Promise.all ile async/await yapısını hibrit kullanmak:
async function verileriGetirHizli() {
// İki isteği aynı anda PARALEL başlatıyoruz!
const kullaniciPromise = fetch('/api/kullanici');
const urunlerPromise = fetch('/api/urunler');
// İkisinin aynı anda bitmesini bekliyoruz
const [kullanici, urunler] = await Promise.all([kullaniciPromise, urunlerPromise]);
// Toplam Süre: ~2 saniye!
}Neden Bu Yaklaşım Doğru?
Buradaki kritik nüans şudur: fetch fonksiyonunu çağırdığınız anda isteğiniz anında arka planda (Web API seviyesinde) başlatılır.
İlk örnekte isteğin önüne await koyduğumuz için JavaScript motoru 1. isteğin bitmesini bekleyip 2. isteği sonradan başlatıyordu. İkinci örnekte ise önce iki isteği de await kullanmadan aynı anda tetikliyor (paralel başlatıyor), ardından Promise.all ile iki arka plan işleminin de tamamlanma anını tek bir noktada bekliyoruz.
Birbirine bağımlı olmayan tüm asenkron işlemlerde bu deseni kullanmak, uygulamanızın toplam yanıt süresini en yavaş olan tek bir isteğin süresine indirger ve ağ (network) kaynaklarını maksimum verimle kullanmanızı sağlar.
Özetle; Callback'lerden Promise'lere, oradan da Async/Await yapısına uzanan bu evrim, asenkron kodları daha okunabilir ve güvenli yazmamızı sağladı.
Peki, kod seviyesinde yazdığımız bu setTimeout, fetch veya Promise yapıları arka planda tarayıcı motoru (V8) tarafından nasıl sıralanıyor ve Call Stack'e hangi kuralla geri gönderiliyor? Şimdi Event Loop ve Görev Kuyrukları (Queues) mimarisini inceleyelim.
