Node async await akışı ilk bakışta kafa karıştırıcı görünür: callback'ler, Promise'ler ve async/await aynı işi farklı yollarla yapar gibidir. Aslında bunlar aynı problemin üç kuşağıdır. Node.js tek iş parçacığında (single-thread) çalışır ve dosya okuma, veritabanı sorgusu veya HTTP isteği gibi işlemler zaman alır. Bu yazıda asenkron akışın neden gerekli olduğunu, callback cehenneminden Promise'lere ve oradan async/await'in temiz sözdizimine nasıl geldiğimizi, hataları nasıl yöneteceğini ve işleri nasıl paralel çalıştıracağını öğreneceksin.
Neden asenkron? Event loop'a kısa bir bakış
Node.js, tek bir ana iş parçacığı üzerinde çalışır ve bu iş parçacığını bloklamamak üzerine kuruludur. Bir dosyayı diskten okumak milisaniyeler sürer; eğer Node bu süre boyunca beklerse, o esnada gelen başka hiçbir isteği işleyemez. Bunun yerine işlemi işletim sistemine devreder, kendisi diğer işlere bakar ve sonuç hazır olduğunda geri döner. Bu mekanizmaya event loop denir.
Önemli ayrım şudur: asenkron kod eşzamanlı (paralel) çalışan iş parçacığı demek değildir. Tek bir thread vardır; sadece bekleme süreleri akıllıca yönetilir. Bu yüzden uzun süren CPU hesaplaması (örneğin büyük bir döngü) event loop'u yine de bloklar — async/await burada yardımcı olmaz; o yalnızca I/O bekleyen işlemler için tasarlanmıştır.
Bu modelin pratik sonucu şudur: Node.js, az sayıda iş parçacığıyla binlerce eş zamanlı bağlantıyı verimli biçimde karşılayabilir. Sırrı, "her isteğe bir thread" yerine "bekleyen işi devret, hazır olanı işle" yaklaşımıdır. Asenkron sözdiziminin üç kuşağını anlamak, bu modelden en iyi şekilde yararlanmanı sağlar.
Birinci kuşak: callback'ler ve "callback cehennemi"
Node'un ilk yıllarında asenkron işlemler callback fonksiyonlarıyla yönetilirdi. Bir işin sonucu, parametre olarak verdiğin bir fonksiyonla geri gelir. Node'un geleneği "error-first callback"tir: ilk parametre hata, ikincisi sonuçtur.
const fs = require("fs");
fs.readFile("a.txt", "utf8", (err, data) => {
if (err) return console.error(err);
console.log(data);
});
Tek işlem için sorun yok. Ama işlemler birbirine bağımlı hale gelince kod sağa doğru kayan bir piramide dönüşür. Buna callback cehennemi (callback hell) denir:
readFile("a.txt", (err, a) => {
readFile("b.txt", (err, b) => {
readFile("c.txt", (err, c) => {
// her seviyede hata kontrolü tekrar eder
});
});
});
Bu yapı okunması, hatalarını yakalaması ve bakımı zor bir koddur. İşte Promise'ler tam bu sorunu çözmek için geldi.
İkinci kuşak: Promise nedir?
Bir Promise, "henüz hazır olmayan ama gelecekte gelecek bir değeri" temsil eden bir nesnedir. Üç durumu vardır: pending (beklemede), fulfilled (başarıyla tamamlandı) ve rejected (hata ile sonuçlandı). Sonucu .then(), hatayı .catch() ile yakalarsın:
const fs = require("fs/promises");
fs.readFile("a.txt", "utf8")
.then((data) => console.log(data))
.catch((err) => console.error(err));
Promise'lerin asıl gücü zincirleme (chaining) yeteneğidir. Her .then() yeni bir Promise döndürür, böylece piramit yerine düz bir akış elde edersin. Yine de uzun zincirlerde okunabilirlik bir noktadan sonra düşer — ve burada async/await devreye girer.
Üçüncü kuşak: node async await ile senkron görünümlü kod
async/await, Promise'lerin üzerine inşa edilmiş bir sözdizimsel kolaylıktır (syntactic sugar); altta hâlâ Promise'ler çalışır. Bir fonksiyonu async ile işaretlersen, içinde await kullanabilirsin. await, bir Promise çözülene kadar o satırda "bekler" ama event loop'u bloklamaz.
const fs = require("fs/promises");
async function oku() {
const a = await fs.readFile("a.txt", "utf8");
const b = await fs.readFile("b.txt", "utf8");
return a + b;
}
Bu kod, callback piramidiyle aynı işi yapar ama yukarıdan aşağıya, senkron bir kod gibi okunur. Dikkat edilmesi gereken üç kural var:
awaityalnızcaasyncbir fonksiyonun içinde (ya da modern Node'da üst seviye modüllerde) kullanılabilir.- Bir
asyncfonksiyon her zaman bir Promise döndürür; içinde nereturnedersen et, dışarıdanawaitya da.then()ile alınır. awaitsadece I/O'yu hızlandırmaz; CPU yoğun işlemleri hâlâ ana thread'i bloklar.
Hata yönetimi: try/catch ile temiz yakalama
Callback'lerde her seviyede if (err) yazmak zorundaydın. async/await ile sıradan, senkron koddaki gibi try/catch kullanırsın. Bu, async/await'in en büyük pratik kazanımlarından biridir:
async function oku() {
try {
const data = await fs.readFile("yok.txt", "utf8");
return data;
} catch (err) {
console.error("Okuma başarısız:", err.message);
return null;
}
}
Bir await ifadesi reddedilirse (rejected), catch bloğu tetiklenir. Eğer hatayı yakalamazsan, bu bir unhandled rejection'a dönüşür ve modern Node sürümlerinde süreci çökertebilir. Bu yüzden her asenkron işlemin bir hata stratejisi olmalı: ya yerel bir try/catch, ya da çağıran tarafta yakalanan bir Promise reddi.
Paralel çalıştırma: Promise.all ve allSettled
Sık yapılan bir hata, birbirinden bağımsız işleri gereksiz yere sırayla beklemektir. Aşağıdaki kod iki dosyayı art arda okur; ikincisi birincinin bitmesini bekler:
const a = await fs.readFile("a.txt", "utf8"); // önce bu biter
const b = await fs.readFile("b.txt", "utf8"); // sonra bu başlar
İşler birbirine bağımlı değilse, ikisini aynı anda başlatıp birlikte beklemek çok daha hızlıdır. Bunun için Promise.all kullanılır:
const [a, b] = await Promise.all([
fs.readFile("a.txt", "utf8"),
fs.readFile("b.txt", "utf8"),
]);
Promise.all, içindeki işlerden biri bile reddedilirse tümüyle reddolur. Bazı işlerin başarısız olmasına izin verip yine de hepsinin sonucunu görmek istiyorsan, Promise.allSettled kullan; her işin durumunu (fulfilled/rejected) ayrı ayrı döndürür.
Sık yapılan hatalar
- Döngüde
forEachileawait.array.forEach(async ...)beklediğin gibi çalışmaz;forEachPromise'leri beklemez. Sırayla beklemek için klasikfor...of, paralel içinPromise.all(array.map(...))kullan. await'i unutmak.awaitkoymazsan değişkene değer değil, çözülmemiş bir Promise nesnesi atanır.- Gereksiz sıralı bekleme. Bağımsız işleri tek tek
awaitetmek, isteklerini gereksiz yavaşlatır.
Sık Sorulan Sorular
async/await mı yoksa .then() mi kullanmalıyım?
İkisi de aynı Promise altyapısını kullanır; tercih okunabilirlik meselesidir. Çok sayıda bağımlı adımın olduğu akışlarda async/await genelde daha temiz okunur. Tek bir kısa dönüşümde ise .then() pratik olabilir. Aynı kod tabanında tutarlı kalmak en iyisidir.
await kodu yavaşlatır mı?
await tek başına kodu yavaşlatmaz; sadece o Promise çözülene kadar fonksiyonun ilerlemesini erteler ve bu sırada event loop başka işleri yapmaya devam eder. Yavaşlık genelde bağımsız işleri gereksiz yere sırayla beklemekten gelir — bu durumda Promise.all ile paralelleştir.
CPU yoğun işler için async/await yeterli mi?
Hayır. async/await yalnızca I/O beklemelerini verimli yönetir. Büyük bir hesaplama (örneğin şifreleme, görüntü işleme) ana thread'i yine de bloklar. Bunun için worker_threads modülünü veya ayrı bir süreci düşünmelisin.
Node.js projende asenkron akışı düzeltmek mi istiyorsun? Callback cehennemine dönmüş bir kod tabanını async/await ile temizlemek, performansı paralelleştirmeyle artırmak veya sıfırdan sağlam bir API kurmak için benimle iletişime geç.