JavaScript async/await: Okunur Asenkron Kod, Hata Yakalama ve Paralel Bekleme
async fonksiyonların neden hep promise döndürdüğünü, await'in sayfayı dondurmadan nasıl beklediğini, try/catch ile hata yakalamayı, sıralı ve paralel beklemeyi, döngüde await tuzağını ve top-level await'i görüyoruz.
Sürüm notu: Bu yazıdaki kodlar ES2026 ve güncel tarayıcılar (Chrome, Edge, Firefox, Safari) içindir.
Önce Tahmin Et
Bir async fonksiyon, içinde bir await, dışında iki console.log. Rakamlar hangi sırayla yazılır?
async function run() {
console.log("2");
await null;
console.log("4");
}
console.log("1");
run();
console.log("3");
Okumaya devam etmeden önce bir tahmin yap. await null neredeyse hiçbir şey beklemiyor gibi görünüyor; acaba gerçekten hiçbir şey beklemiyor mu? Cevabı await'in ne yaptığını anlattıktan sonra konuşacağız.
async Fonksiyon Her Zaman Promise Döndürür
Bir önceki yazıda promise zincirleriyle callback piramidini düzleştirdik. async/await ise ES2017 ile geldi ve aynı promise'leri, yukarıdan aşağı okunan sıradan bir kod gibi yazmamızı sağladı. Yeni bir asenkron model değil; promise'lerin üzerine kurulmuş bir syntax.
İlk kural şu: bir fonksiyonun önüne async yazdığında o fonksiyon her zaman bir promise döndürür. Bunu konsolda dene:
async function getNumber() {
return 42;
}
const result = getNumber();
console.log(result instanceof Promise); // true
result.then((value) => console.log(value)); // 42
return 42 yazdık ama elimize 42 değil, 42 ile sonuçlanan bir promise geldi. Fonksiyonun içinde throw edersen de dışarıya fırlayan bir hata değil, reddedilmiş bir promise çıkar:
async function fail() {
throw new Error("Something went wrong");
}
fail().catch((error) => console.error(error.message));
async her fonksiyon biçimiyle kullanılabilir: async function load() {}, const load = async () => {} ya da bir class içinde async load() {}.
await: Bekle Ama Sayfayı Dondurma
await, bir promise'in sonuçlanmasını bekler ve onun değerini verir. Sadece async bir fonksiyonun içinde, ya da birazdan göreceğimiz gibi bir module'ün en üst seviyesinde kullanılabilir. Bir önceki yazıdaki wait fonksiyonunu hatırla:
function wait(ms) {
return new Promise((resolve) => setTimeout(() => resolve(`${ms} ms passed`), ms));
}
async function demo() {
console.log("Started");
const message = await wait(1000);
console.log(message); // "1000 ms passed"
console.log("Done");
}
demo();
"Bekle" kelimesi seni korkutmasın. await ana thread'i dondurmaz; sadece bulunduğu fonksiyonu duraklatır. Bir kafede kahveni sipariş ettiğini düşün. Kasiyer fişini verir, sen kenara çekilirsin ve kasiyer bu arada diğer müşterilere bakar. Kahven hazır olunca adın okunur ve kaldığın yerden devam edersin. await da fonksiyonu kenara çeker, call stack boşalır ve event loop başka işlere bakar. Promise sonuçlandığında fonksiyon kaldığı satırdan devam eder.
Şimdi baştaki soruya dönelim. Önce 1 yazılır. run() çağrılınca fonksiyon, ilk await'e kadar senkron çalışır; 2 yazılır. await null'da fonksiyon duraklar ve kontrol çağırana döner, 3 yazılır. Script bitince fonksiyon kaldığı yerden devam eder ve 4 yazılır. Cevap: 1, 2, 3, 4. Promise olmayan bir değeri beklesen bile await onu hazır bir promise'e çevirir ve fonksiyon yine de duraklar; o an çalışan kod bitmeden devam etmez.
Promise Zincirinden async/await'e
Bir önceki yazıdaki kullanıcı, sipariş ve sipariş detayı zincirini hatırla. O yazının son alıştırmasında, son adımda kullanıcının adına ulaşmak için uğraşmıştık. async/await ile bu sorun kendiliğinden ortadan kalkıyor, çünkü her sonuç sıradan bir değişkende duruyor:
function getUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id <= 0) reject(new Error("Invalid user id"));
else resolve({ id, name: "Ellie" });
}, 500);
});
}
function getOrders(userId) {
return wait(500).then(() => [{ id: 101 }, { id: 102 }]);
}
function getOrderDetail(orderId) {
return wait(500).then(() => ({ id: orderId, total: 349.9 }));
}
async function showFirstOrder(userId) {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const detail = await getOrderDetail(orders[0].id);
console.log(`${user.name} paid ${detail.total} on their first order`);
}
showFirstOrder(1);
Ne then var ne iç içe arrow function. Kod, senkron bir kod gibi yukarıdan aşağı okunuyor ama hiçbir satır sayfayı dondurmuyor. Altta yine aynı promise'ler çalışıyor; sadece onları okumak çok daha kolay.
Hata Yakalama: try/catch Geri Döndü
Daha önce gördüğümüz try/catch/finally, await ile birlikte asenkron hataları da yakalar. Reddedilen bir promise'i await ettiğinde, o satır sanki bir throw varmış gibi davranır:
async function showUser(id) {
console.log("Loading...");
try {
const user = await getUser(id);
console.log(`Welcome, ${user.name}`);
} catch (error) {
console.error(`Error: ${error.message}`);
} finally {
console.log("Loading finished");
}
}
showUser(1); // Loading... → Welcome, Ellie → Loading finished
showUser(-1); // Loading... → Error: Invalid user id → Loading finished
Senkron ve asenkron hatalar için artık tek bir araç var. finally burada da yükleniyor göstergesini kapatmak için doğru yer, çünkü iş ister başarıyla ister hatayla bitsin mutlaka çalışır.
Unutma ki async bir fonksiyon promise döndürür. İçeride hatayı yakalamadıysan, dışarıda showUser(1).catch(...) ile yakalayabilirsin. İkisi de yoksa, bir önceki yazıda gördüğümüz "Uncaught (in promise)" hatasıyla karşılaşırsın.
Sıralı mı, Paralel mi?
await'in okunurluğu bir tuzak da getiriyor: her şeyi alt alta beklemek. Aşağıdaki iki fonksiyonu konsolda çalıştırıp sürelerini karşılaştır:
async function sequential() {
console.time("sequential");
const a = await wait(1000);
const b = await wait(1000);
const c = await wait(1000);
console.timeEnd("sequential"); // yaklaşık 3000 ms
}
async function parallel() {
console.time("parallel");
const [a, b, c] = await Promise.all([wait(1000), wait(1000), wait(1000)]);
console.timeEnd("parallel"); // yaklaşık 1000 ms
}
sequential().then(parallel);
Birincisinde her wait, bir öncekinin bitmesini bekliyor ve süreler toplanıyor. İkincisinde üç iş aynı anda başlıyor ve Promise.all en uzun olanı kadar bekliyor. Daha önce gördüğümüz array destructuring ile sonuçları tek satırda ayırdık.
Hangisini seçeceğin işlerin birbirine bağlı olup olmadığına bağlı. showFirstOrder'da siparişleri istemek için kullanıcının id'sine ihtiyacımız vardı; orada sıralı beklemek zorunlu. Ama bir sayfada kullanıcı profili, bildirimler ve hava durumu gibi birbirinden habersiz üç veriyi çekiyorsan, onları sırayla beklemek kullanıcıya boşuna iki kat süre bekletir.
Döngüde await Tuzağı
Bir id array'i için veri çekmek istediğinde ilk akla gelen forEach olur. Ama forEach, kendisine verilen fonksiyonun promise döndürüp döndürmediğiyle ilgilenmez:
async function loadAll() {
const ids = [1, 2, 3];
ids.forEach(async (id) => {
const user = await getUser(id);
console.log(user.id);
});
console.log("All loaded"); // önce bu yazılır!
}
loadAll();
"All loaded" ilk sırada çıkar, çünkü forEach üç async fonksiyonu başlatıp beklemeden geçer. Bekleme gerçekten gerekiyorsa iki doğru yol var. Sıralı istiyorsan for...of kullan; await orada döngüyü gerçekten duraklatır. Paralel istiyorsan daha önce gördüğümüz map ile bir promise array'i üret ve Promise.all ile bekle:
async function loadSequential(ids) {
for (const id of ids) {
const user = await getUser(id);
console.log("sequential:", user.id);
}
console.log("Sequential load finished");
}
async function loadParallel(ids) {
const users = await Promise.all(ids.map((id) => getUser(id)));
console.log("parallel:", users.map((user) => user.id));
}
loadSequential([1, 2, 3]).then(() => loadParallel([1, 2, 3]));
Sıralı versiyon yaklaşık bir buçuk saniye, paralel olan yarım saniye sürer. Döngüde sıralı beklemek bazen tam istediğindir; örneğin bir sunucuya aynı anda çok istek atmak istemiyorsan. Önemli olan bunu bilerek seçmek.
Top-Level await: Module'lerin En Üstünde
ES2022 ile await'i bir fonksiyonun dışında, module'ün en üst seviyesinde de kullanabiliyoruz. Bunun için script'in bir module olması gerekir. Aşağıdaki dosyayı kaydedip tarayıcıda aç:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Top-level await</title>
</head>
<body>
<p id="out">Waiting...</p>
<script type="module">
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
await wait(1500);
document.querySelector("#out").textContent = "Ready after 1.5 seconds";
</script>
</body>
</html>
type="module"'ı silersen tarayıcı await satırında SyntaxError verir; klasik script'lerde top-level await yoktur. Tarayıcının DevTools konsolu ise kolaylık olsun diye en üst seviyede await yazmana izin verir; bu yüzden konsolda await wait(1000) çalışır. Module'lerin geri kalanını, import ve export ile birlikte, ilerleyen yazılarda ayrıntılı göreceğiz.
Sık Düşülen Tuzaklar
Birincisi await'i unutmak. Hata vermez, ama elinde değer yerine bir promise olur:
async function broken() {
const user = getUser(1); // await yok
console.log(user.name); // undefined, çünkü user bir Promise
}
broken();
İkincisi, async olmayan bir fonksiyonun içinde await kullanmak. Bu bir SyntaxError'dır ve en sık, async bir fonksiyonun içindeki sıradan bir callback'te karşına çıkar. Dıştaki fonksiyonun async olması içtekini kapsamaz; await'in bulunduğu fonksiyonun kendisi async olmalı.
Üçüncüsü, birbirinden bağımsız işleri gereksiz yere sıralı beklemek. Kod doğru çalışır, sadece yavaştır; bu yüzden fark etmesi de zordur. Alt alta birkaç await gördüğünde "bunlar birbirinin sonucuna ihtiyaç duyuyor mu?" diye sor.
Dördüncüsü, async bir fonksiyonu çağırıp ne await ne de catch eklemek. İçeride bir hata olursa kimse yakalamaz. Bir event handler içinde loadData() diye çağırıyorsan, hatayı ya fonksiyonun içinde try/catch ile ya da dışarıda .catch() ile yakala.
Küçük Bir Deneme
★☆☆ Bir önceki yazıdaki "One", "Two", "Three" alıştırmasını async/await ve wait ile yeniden yaz. Hangisini okumak daha kolay?
★★☆ Çıktıyı ve sırasını tahmin et, sonra dene:
async function a() {
console.log("a1");
await wait(0);
console.log("a2");
}
async function b() {
console.log("b1");
await null;
console.log("b2");
}
a();
b();
console.log("end");
★★★ getUser'ı kullanarak [1, 2, -1, 3] id'leri için paralel istek atan bir fonksiyon yaz. Promise.all bir hata yüzünden tamamen reddedilmesin; her id için ya kullanıcının adını ya da "error" yazan bir sonuç array'i üret. İpucu: her promise'i Promise.all'a vermeden önce kendi catch'iyle sarabilirsin.
Şimdiye kadar beklediğimiz her şey setTimeout ile taklit edilmiş sahte işlerdi. Bir sonraki yazıda gerçek bir sunucuyla konuşacağız: Fetch API ile veri çekip göndermeyi, HTTP durum kodlarını, zaman aşımını ve yükleniyor ile hata durumlarını ekranda göstermeyi göreceğiz.