JavaScript'te async/await ile okunur asenkron kod
Promise zincirlerinin yerini alan async/await sözdiziminin JavaScript'teki asenkron kodu nasıl okunabilir kıldığını örneklerle açıklıyorum.
JavaScript’te asenkron kod yazmak uzun süre boyunca “callback hell” olarak anılan bir sorunu beraberinde getirdi. Bir işlem tamamlanınca başka bir işlem, o bitince bir diğeri — ve bunlar iç içe geçtikçe kod hem yazmak hem okumak için giderek zorlaşıyordu. Promise’ler bu sorunu büyük ölçüde hafifletdi; ama uzun .then() zincirleri de kendi karmaşıklığını getirdi. async/await sözdizimi ise bu problemi gerçekten çözdü.
Önce hatırlayalım: Promise zinciri
2017 yazısında Promise’lerden söz etmiştim. Bir API’den veri çekip işleyen bir senaryoda Promise zinciri şöyle görünebiliyordu:
fetchUser(userId)
.then(user => fetchPosts(user.id))
.then(posts => filterActive(posts))
.then(activePosts => renderList(activePosts))
.catch(err => console.error(err));
Bu okunabilir bir zincir. Ama hata yönetimi daha karmaşık hâle geldiğinde, bazı adımlarda hem başarı hem hata durumunu ayrı ayrı ele almanız gerektiğinde, ya da döngü içinde asenkron çağrı yapmanız gerektiğinde, .then() zinciri hızla dağınıklaşıyor.
async/await nedir?
async/await, Promise tabanlı asenkron kodu senkronmuş gibi yazmanızı sağlayan bir sözdizim şekeridir. Arka planda hâlâ Promise kullanılır; sadece yazım biçimi değişir.
Bir fonksiyonu async ile işaretlerseniz, içinde await kullanabilirsiniz. await, bir Promise resolve edilene kadar o satırda bekler ve sonucu doğrudan döndürür.
Yukarıdaki zinciri async/await ile yazalım:
async function loadUserPosts(userId) {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const activePosts = filterActive(posts);
renderList(activePosts);
}
Fark açık: satırlar yukarıdan aşağıya akıyor. Hangi adımın hangi veriyi aldığı, hangi adımın hangisine bağımlı olduğu göz önünde.
Hata yönetimi
async/await ile hata yönetimi try/catch bloğuyla yapılıyor. Promise zincirinin .catch() metoduna karşılık geliyor:
async function loadUserPosts(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
renderList(filterActive(posts));
} catch (err) {
console.error('Veri yüklenemedi:', err.message);
}
}
Bu yapının bir avantajı, senkron hataları da (örneğin filterActive içinde bir TypeError) aynı catch bloğunun yakalaması. Promise zincirinde bunu elde etmek için ekstra dikkat gerektiriyordu.
Bir tuzak noktası şu: async fonksiyonu çağırırken dışarıda da await yoksa, hata çağrı noktasında yakalanmıyor ve sessizce yutuluyor. Özellikle event listener içinden async fonksiyon çağırırken bu duruma dikkat etmek gerekiyor:
// Dikkatli olun: hata burada sessizce kaybolabilir
button.addEventListener('click', async () => {
await doSomething(); // hata throw olsa da dışarı çıkmaz
});
Bunun için en azından genel bir .catch() veya window-level hata dinleyicisi bulundurmak iyi bir alışkanlık.
Paralel çağrılar için Promise.all
await ile ardışık çağrılar kolaylaşıyor ama dikkat edilmesi gereken bir nokta var: her await bir önceki işlemin bitmesini bekler. İki bağımsız çağrıyı art arda await ile yazarsanız, ikincisi birinciyi bekliyor demektir.
// YANLIŞ: gereksiz sıralı bekleme
const user = await fetchUser(id);
const settings = await fetchSettings(id);
Bu ikisi bağımsız olduğunda Promise.all daha uygun:
// DOĞRU: ikisi paralel başlar
const [user, settings] = await Promise.all([
fetchUser(id),
fetchSettings(id)
]);
Promise.all bir dizi Promise alır, hepsini paralel olarak başlatır ve hepsi tamamlandığında sonuçları dizi olarak döndürür.
Döngü içinde asenkron işlem
Asenkron kodun önceki yazım biçimlerinde en çok kafa karıştıran konu döngülerdi. async/await bunu önemli ölçüde sadeleştiriyor:
async function processItems(items) {
for (const item of items) {
await processOne(item);
}
}
Her öğe işlenince bir sonrakine geçiyor. Sıralı işleme istiyorsanız bu yeterli. Paralel işleme istiyorsanız Promise.all ile map kombinasyonuna başvurmak gerekiyor.
Dikkat edilmesi gereken bir nokta: forEach döngüsü async/await ile beklediğiniz gibi çalışmıyor. Array.prototype.forEach callback’in döndürdüğü Promise’i beklemeden devam ediyor. Döngü içinde await kullanmak istiyorsanız for...of ya da for döngüsü kullanın.
Tarayıcı ve Node.js desteği
async/await, ES2017 (ES8) standardıyla geldi. 2018 itibarıyla modern tarayıcıların büyük çoğunluğu doğrudan destekliyor. Eski tarayıcıları desteklemek gerekiyorsa Babel ile transpile edilebilir. Node.js tarafında ise 7.6 sürümünden itibaren sorunsuz çalışıyor.
Sonuç
async/await’in benim için en büyük kazanımı, asenkron kodu “okurken” beyin gücü harcamamak. Promise zincirini çözerken zihinsel olarak bir yüklenme oluyordu; async/await ile kod neredeyse düzyazı gibi akıyor. Bu sözdizim şekerine geçtikten sonra geri dönmek istemedim.