JavaScript'te Promise ve asenkron akış
JavaScript'te Promise nedir, callback yığınından nasıl kurtulunur; then, catch zincirleme ve temel asenkron akış kalıpları.
JavaScript’te asenkron işlemler her zaman biraz tuhaf hissettirmiştir. Dil single-threaded çalışıyor ama AJAX istekleri, zamanlayıcılar ve dosya okuma gibi işlemler bloklamaksızın gerçekleşiyor. Bu durumla başa çıkmak için yıllarca callback kullandık. İşe yarıyor, ama bir noktada okunamaz hale geliyor.
Promise, JavaScript’te ES6 ile dile eklenen, asenkron işlemin gelecekteki sonucunu temsil eden bir nesne. Henüz tamamlanmamış, başarıyla tamamlanmış ya da başarısız olmuş üç durumdan birinde bulunuyor. Bu yapı, callback zincirine karşı daha okunur ve yönetilebilir bir alternatif sunuyor.
Callback cehennemi
Önce sorunun kaynağını görmek için şöyle bir senaryo düşünelim: Kullanıcı verisini çek, sonra bu kullanıcının siparişlerini çek, sonra her siparişin ürünlerini çek.
getUser(userId, function(user) {
getOrders(user.id, function(orders) {
getProducts(orders[0].id, function(products) {
// Bir şeyler yap
}, function(err) {
console.error('Ürün hatası:', err);
});
}, function(err) {
console.error('Sipariş hatası:', err);
});
}, function(err) {
console.error('Kullanıcı hatası:', err);
});
Her adımda iki callback gerekiyor: biri başarı için biri hata için. Girintiler derinleştikçe okumak güçleşiyor. Buna “callback cehennemi” ya da “piramit of doom” (ölüm piramidi) deniliyor.
Promise ile aynı akış
getUser(userId)
.then(function(user) {
return getOrders(user.id);
})
.then(function(orders) {
return getProducts(orders[0].id);
})
.then(function(products) {
// Ürünlerle bir şeyler yap
})
.catch(function(err) {
console.error('Bir hata oluştu:', err);
});
Tüm hatalar tek bir catch bloğunda yakalanıyor. Zincirdeki herhangi bir adımda hata oluşursa doğrudan catch’e düşüyor. Kod soldan sağa, yukarıdan aşağıya okunuyor.
Promise oluşturmak
Kendi fonksiyonunuzu Promise döndürecek şekilde yazmak için new Promise() yapısı kullanılıyor:
function delay(ms) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Bekleme tamamlandı');
}, ms);
});
}
delay(1000).then(function(message) {
console.log(message); // "Bekleme tamamlandı"
});
resolve çağrıldığında Promise başarıya ulaşıyor ve .then() tetikleniyor. reject çağrıldığında veya bir exception fırlatıldığında .catch() tetikleniyor.
Promise.all ile paralel istekler
Birden fazla işlemi aynı anda başlatıp hepsini beklemeniz gerektiğinde Promise.all kullanılıyor:
var userRequest = fetch('/api/user/1');
var settingsRequest = fetch('/api/settings');
Promise.all([userRequest, settingsRequest])
.then(function(responses) {
var userResponse = responses[0];
var settingsResponse = responses[1];
// Her ikisi de hazır
})
.catch(function(err) {
// Herhangi biri başarısız olursa buraya düşer
});
Sırayla beklemek yerine ikisi paralel gidiyor; toplam süre en uzun isteğin süresine eşit oluyor.
Promise.all’ın bir kısıtlaması var: dizideki herhangi bir Promise başarısız olursa tüm grup başarısız sayılıyor ve catch’e düşüyor. Bazı işlemlerin başarısız olmasına izin verip diğerlerine devam etmek istiyorsanız Promise.allSettled daha uygun — ama bu ES2020’de geldi; 2017’de elle ele almak gerekiyordu.
Dikkat edilmesi gereken noktar
Promise zincirinde return unutmak yaygın bir hata. .then() içinde yeni bir Promise döndürmezseniz zincirin geri kalanı o Promise’i beklemeden devam eder:
// Hatalı: return yok
.then(function(user) {
getOrders(user.id); // Bu Promise beklenmeden geçilir
})
// Doğru: return var
.then(function(user) {
return getOrders(user.id);
})
Bu hatayı yapmak kolay ve debugging sırasında görünmesi güç. Semptom şöyle: orders parametresi then’e undefined olarak geliyor çünkü önceki adım hiçbir şey döndürmedi. Kodu ilk kez okuyanlar genellikle birkaç dakika bunu aramak zorunda kalıyor.
ES6 ok fonksiyonlarıyla
Arrow function ile zincir daha kısa görünüyor:
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getProducts(orders[0].id))
.then(products => console.log(products))
.catch(err => console.error(err));
Ok fonksiyonunda tek satırlık bir ifade yazınca return örtük olarak çalışıyor. Bu, zincirdeki return unutma hatasını da azaltıyor; ama çok satırlı blok kullanınca yine return gerekiyor.
Promise’i tam anlamıyla kavramak biraz zaman alıyor; özellikle zincirleme davranışını zihinsel olarak modellemek ilk başta güç hissettiriyor. PHP’de senkron kodu satır satır takip etmek alışılagelmiş bir şey; “bu fonksiyon şu an çalışmıyor, sonra çalışacak” fikrini içselleştirmek bir paradigma geçişi gerektiriyor. Ama bir kez yerleşince callback yığınlarına dönmek istemiyorsunuz.
Promise, bir değer değil bir “değerin geleceğine dair söz”dür. Bunu içselleştirince .then() zincirinin neden her adımda yeni bir Promise döndürdüğü anlaşılıyor. Değer henüz yok; ne zaman hazır olacağını bilmiyorsunuz, ama hazır olduğunda ne yapılacağını önceden tanımlamış oluyorsunuz. Bu “gelecekteki işlemi şimdiden tarif etme” alışkanlığı, bir süre sonra çok daha geniş bir programlama zihniyetinin kapısını açıyor. 2017’de bunu tam kavramak biraz zaman aldı; ama o kavrayış sonrasında JavaScript’le ilişkim değişti.