JavaScript'te async hataları, senkron hatalar gibi davranmaz
Senkron JavaScript'te fırlatılan bir hata, bir try/catch tarafından yakalanana kadar çağrı yığınında yukarı doğru yayılır; yakalanmazsa program çöker. Async kod bu modeli bozar. Bir ağ isteği başarısız olduğunda, o isteği başlatan fonksiyon çoktan geri dönmüştür. Yani hatanın yayılabileceği bir çağrı yığını artık ortada yoktur.
Promise'ler bu sorunu, hatalara kendilerine ait bir kanal vererek çözer. Bir promise ya bir değerle fulfill olur ya da bir sebeple reject edilir. Reject etmek, async dünyanın throw'a karşılık gelen halidir. Bu sayfadaki her şeyin amacı, reject'lerin sessizce kaybolmak yerine senin kontrolünde bir yere düşmesini sağlamak.
try/catch bloğu sorunsuz çalışıp bitiyor. Reject ise 50ms sonra, yani try bloğu çoktan bittikten sonra gerçekleşiyor. Hatayı yakalayan kimse yok. İşte tuzak burada.
await ile try/catch Yeniden İşe Yarıyor
Bir promise'i await ettiğiniz an, reject durumu async fonksiyonun içinde fırlatılmış bir hataya dönüşür. Etrafa sardığınız try/catch de onu tıpkı senkron bir throw gibi yakalar:
İlk tercihin bu desen olmalı. await, asenkron dünyayı bize tanıdık gelen try/catch yapısına bağlar. Hata fırlatabilecek await çağrılarını try bloğuna koy, catch ile de yakala.
Dikkat edilmesi gereken bir ayrıntı var: sadece await ile beklenen çağrı kapsama girer. Eğer bir promise'i await etmeden başlatırsan, hatalar yine dışarı sızar.
En Sık Yapılan Hata: await Yazmayı Unutmak
Bir async fonksiyonu await kullanmadan (ya da döndürdüğü promise'i return etmeden) çağırırsan, reject olan promise çevresindeki try/catch'e takılmaz:
try bloğu sorunsuz bitiyor. Reject işlemi bir sonraki tick'te gerçekleşiyor ve onu yakalayacak hiçbir şey yok. Sonuç olarak konsolda "unhandled promise rejection" uyarısını görürsün.
Çözüm her zaman aynı: çağrının önüne await koy ya da promise'i return et ki çağıran taraf await edebilsin.
.catch() Aynı Madalyonun Diğer Yüzü
async/await kullanmadan da .catch() zincirleyerek promise reject yakalayabilirsin:
.catch(fn) aslında .then(undefined, fn)'nin kısaltılmış hali. Zincirin daha önceki adımlarında oluşan herhangi bir rejection'ı yakalar. Zincirin sonuna koyduğun bir .catch(), üst seviyedeki bir try/catch'in async dünyasındaki karşılığı sayılır — rejection "unhandled" damgasını yemeden önceki son savunma hattın.
İki stili birlikte kullanmakta hiçbir sakınca yok. Sık rastlanan bir kalıp şudur: fonksiyonun içinde async/await kullanırsın, çağıran taraf da sonuna .catch() ekler:
fetch, HTTP hatalarında reject etmez
Bu tuzağa herkes en az bir kere düşer. fetch, yalnızca ağ seviyesindeki hatalarda reject eder — DNS çözümlemesi başarısız olduğunda, bağlantı reddedildiğinde ya da istek iptal edildiğinde. 404 veya 500 dönen bir yanıt, fetch açısından başarılı sayılır. Promise resolve olur; sadece ok değeri false olan bir response ile resolve olur.
catch bloğunda HTTP hatalarını da yakalamak istiyorsan, res.ok kontrolü yapıp elle hata fırlatman gerekir:
Bu kalıbı iki kez yazdığınızı fark ettiğiniz an, bir yardımcı fonksiyona taşımaya değer.
Promise.all hızlı patlar, Promise.allSettled patlamaz
Promise.all bir promise dizisi alır ve sonuçların dizisiyle resolve olur — ta ki içlerinden biri reject edene kadar. O durumda anında o hatayla reject eder. Diğer promise'ler çalışmaya devam eder ama sonuçları çöpe gider.
Fail-fast davranışı, tüm sonuçlara ihtiyaç duyduğunuz ve tek bir hatanın işlemin tamamını anlamsız kıldığı durumlarda doğru seçimdir. Ama bazen sonuçların hepsini görmek istersiniz — "şu beş yüklemeyi dene, hangisi başarılı hangisi başarısız olmuş bana söyle" gibi. İşte bu tür senaryolarda Promise.allSettled devreye girer:
allSettled asla reject etmez. Her bir sonuç ya {status: "fulfilled", value} ya da {status: "rejected", reason} şeklinde döner.
Hatayı Yeniden Fırlatma ve Dar Kapsamlı Catch
Her hata aynı yere düşmemeli. Yaygın bir yaklaşım şudur: hatayı yakala, incele ve beklemediğin bir şeyse tekrar fırlat:
Her hatayı boş bir catch (err) {} ile yutmak, gerçek bug'ları gözden kaçırmanıza yol açar. Sadece anlamlı şekilde ele alabileceğiniz hataları yakalayın; geri kalanını yeniden fırlatın.
Son Savunma Hattınız: Unhandled Rejection'lar
Ne kadar dikkatli kod yazarsanız yazın, er ya da geç bir hata elinizden kaçar. Neyse ki hem Node.js hem de tarayıcılar, kimsenin yakalamadığı promise reject'leri için global bir hook sunuyor:
// Tarayıcı
window.addEventListener("unhandledrejection", event => {
console.error("unhandled:", event.reason);
event.preventDefault(); // varsayılan konsol uyarısını bastır
});
// Node.js
process.on("unhandledRejection", reason => {
console.error("unhandled:", reason);
});
Bu, düzgün hata yönetiminin yerine geçmez; olsa olsa son çare olarak log veya telemetri kancası işini görür. Modern Node.js'te yakalanmamış bir rejection, varsayılan olarak süreci çökertir; production'da da genelde istediğiniz şey budur. Hatayı loglayın, sürecin ölmesine izin verin ve temiz bir şekilde yeniden başlatın.
Pratik Kontrol Listesi
Bir async fonksiyon, hata verebilecek bir şeye dokunduğunda kendinize şunları sorun:
- Risk taşıyan her
awaitbirtry/catchiçinde mi, yoksa dönen promise'i çağıran taraf.catch()ile mi ele alıyor? - Çağrıyı gerçekten
awaitediyor muyum, yoksa yanlışlıkla fire-and-forget mi yaptım? - Özellikle
fetchkullanırken, cevaba güvenmeden önceres.okkontrolünü yapıyor muyum? - Paralel çalıştırmada doğru araç
Promise.allmi, yoksaPromise.allSettledmı işime yarar? - En üst seviyede bir
.catch()ya daunhandledrejectionhandler'ı var mı ki hiçbir hata sessizce kaybolmasın?
Bu beşini doğru yaptığınız an, async kodunuz artık event loop'a karışıp yok olan hatalarla sizi şaşırtmayı bırakır.
Sırada: ES Modülleri
Async hata yönetimi ile async konusunun son halkasını da tamamladık. Şimdi sırada JavaScript kodunun dosyalar arasında nasıl organize edildiği var: import, export ve her modern projenin temelini oluşturan modül sistemi.
Sıkça Sorulan Sorular
Async bir fonksiyonda hatalar nasıl yakalanır?
await çağrılarını bir try/catch bloğu içine alırsın. Bekletilen (awaited) bir promise reject olduğunda bu, fırlatılan bir hataya dönüşür ve catch bloğuna düşer. Alternatif olarak hatayı yukarı bırakıp, çağıran tarafta dönen promise'in üstüne .catch() ekleyerek de halledebilirsin.
try/catch hatayı neden yakalamıyor?
Büyük ihtimalle hata, await edilmeyen bir kodun içinde oluşuyor. Bir async fonksiyonu await etmeden (ya da döndürdüğü promise'i return etmeden) çağırırsan, oluşan reject dıştaki try/catch'in menzilinden çıkar. Hata yakalamak istediğin promise'i mutlaka await et ya da return et.
Bir promise reject olur ve kimse yakalamazsa ne olur?
Yakalanmamış bir rejection (unhandled rejection) alırsın. Node.js unhandledRejection olayını tetikler ve güncel sürümlerde varsayılan olarak süreci çökertir. Tarayıcıda ise window.onunhandledrejection tetiklenir ve konsola uyarı düşer. Her iki durumda da çözüm aynı: ya .catch() ekle ya da bir await etrafında try/catch kullan.
Promise.all hataları nasıl ele alır?
Promise.all, giriş promise'lerinden herhangi biri reject olur olmaz kendisi de reject olur; diğer promise'ler çalışmaya devam eder ama sonuçları çöpe gider. Hata olsa da olmasa da tüm sonuçları görmek istiyorsan Promise.allSettled kullan — bu yöntem sana {status, value} ya da {status, reason} nesnelerinden oluşan bir dizi döndürür.