JavaScript event loop (olay döngüsü), tek iş parçacığında çalışan JavaScript'in asenkron işleri sırayla yürütmesini sağlayan mekanizmadır. Çalışma ortamı bir kuyruktan iş (job) alır, onu çağrı yığını (call stack) tamamen boşalana kadar kesintisiz çalıştırır, sonra sıradakine geçer. Promise işleyicileri ve await devamları microtask kuyruğuna, setTimeout gibi zamanlayıcılar ise task kuyruğuna girer; her task'tan sonra microtask kuyruğu tamamen boşaltılır.
Bu yazıda MDN'in JavaScript yürütme modeli (execution model) sayfasını temel alarak yığını, kuyruğu, "run-to-completion" kuralını ve microtask ile task arasındaki farkı anlatıyoruz. Tüm sıralama örnekleri Node.js 24'te çalıştırıldı ve çıktıları gerçek sonuçtur.
Motor ve çalışma ortamı (host)
JavaScript'in çalışması iki parçanın iş birliğine dayanır: dili uygulayan JavaScript motoru ve dış dünyayla bağlantıyı sağlayan çalışma ortamı (host). Tarayıcıda DOM ve zamanlayıcılar, sunucuda ise Node.js host görevini üstlenir. ECMAScript spesifikasyonu "job" kavramını ve Promise işlerinin sırasını tanımlar; olay döngüsünün tam ayrıntıları ise HTML standardı gibi host belgelerinde yer alır. Node.js de bu modeli büyük ölçüde taklit eder.
Yığın, heap ve kuyruk

Spesifikasyonda JavaScript'i bağımsız olarak çalıştıran her birime agent denir. MDN, bir agent'ı kabaca bir iş parçacığına benzetir ve her agent'ın üç yapısı olduğunu anlatır:
| Yapı | Ne tutar? | Çalışma biçimi |
|---|---|---|
| Heap | Oluşturulan nesneler | Büyük, yapılandırılmamış bellek bölgesi |
| Kuyruk (job queue) | Çalışmayı bekleyen işler | Genelde ilk giren ilk çıkar (FIFO) |
| Yığın (call stack) | Yürütme bağlamları (stack frame) | Son giren ilk çıkar (LIFO) |
Bir fonksiyon çağrıldığında yığına yeni bir çerçeve eklenir, fonksiyon dönünce çıkarılır. Her iş boş bir yığınla başlar ve yığın yeniden boşaldığında tamamlanmış sayılır. Fonksiyonların değişken ortamını nasıl hatırladığını closure yazımızda, belleğin nasıl temizlendiğini bellek yönetimi yazımızda bulabilirsiniz.
Run-to-completion: bir iş bitmeden diğeri başlamaz
Event loop'un en önemli garantisi şudur: kuyruktan alınan her iş, başka bir iş başlamadan önce sonuna kadar çalışır. Bir fonksiyon çalışırken başka bir kod araya girip onun kullandığı verileri değiştiremez. MDN bunu C gibi dillerdeki iş parçacıklarıyla karşılaştırır: orada bir fonksiyon her an durdurulup başka kod çalıştırılabilir.
const promise = Promise.resolve();
let i = 0;
promise.then(() => { i += 1; console.log(i); });
promise.then(() => { i += 1; console.log(i); });
// 1
// 2İki işleyici yarış durumu (race condition) yaratacakmış gibi görünse de çıktı her zaman 1 ve 2'dir, çünkü her işleyici bir bütün olarak çalışır.
Bu modelin bedeli de vardır: bir iş uzun sürerse kuyruktaki diğer her şey bekler. Aşağıdaki örnekte zamanlayıcı 0 ms ile kurulmuş olsa da senkron döngü bitene kadar çalışamaz:
const basla = Date.now();
setTimeout(() => console.log('zamanlayıcı', Date.now() - basla >= 500), 0);
while (Date.now() - basla < 500) {}
console.log('döngü bitti');
// döngü bitti
// zamanlayıcı trueSpesifikasyon da aynı kuralı koyar: bir agent'ta aynı anda yalnızca bir job değerlendirilebilir ve başlayan bir job, başka bir job başlamadan tamamlanmalıdır.
Microtask ve task kuyruğu

MDN'e göre işler her zaman eşit öncelikle alınmaz. HTML olay döngüsü işleri ikiye ayırır: task ve microtask. Microtask'ların önceliği yüksektir; task kuyruğundan yeni bir iş alınmadan önce microtask kuyruğu tamamen boşaltılır. ECMAScript spesifikasyonundaki bir not da web tarayıcılarının ve Node.js'in Promise işlerini diğer işlerden daha yüksek öncelikle ele aldığını belirtir.
| Kaynak | Kuyruk | Not |
|---|---|---|
| then, catch, finally işleyicileri | Microtask | Promise sonuçlanınca sıraya girer |
| await sonrası kod | Microtask | Değer zaten hazır olsa bile |
| queueMicrotask(fn) | Microtask | Promise işleriyle aynı öncelik |
| setTimeout, setInterval | Task | Süre 0 olsa bile sonraki turda |
| G/Ç ve olay geri çağrıları | Task | Host tarafından eklenir |
Aşağıdaki örnek, sırayı somut olarak gösterir. Numaralar gerçek çıktı sırasıdır:
console.log('1 senkron başlangıç');
setTimeout(() => console.log('6 task: setTimeout'), 0);
Promise.resolve()
.then(() => console.log('3 microtask: then 1'))
.then(() => console.log('5 microtask: then 2'));
queueMicrotask(() => console.log('4 microtask: queueMicrotask'));
console.log('2 senkron bitiş');
// 1 senkron başlangıç
// 2 senkron bitiş
// 3 microtask: then 1
// 4 microtask: queueMicrotask
// 5 microtask: then 2
// 6 task: setTimeoutBurada dikkat edilecek nokta, "then 2" işleyicisinin "then 1" çalışıp bitene kadar kuyruğa girmemesidir. Bu yüzden queueMicrotask ile eklenen iş araya girer. setTimeout ise tüm microtask'lar bitene kadar bekler.
await ve microtask sırası
Bir async fonksiyon await ile karşılaştığında kontrol çağırana döner; bekleme bitince fonksiyonun devamı yeni bir microtask olarak sıraya girer. MDN'e göre bu, beklenen değer zaten çözülmüş bir Promise ya da Promise olmayan bir değer olsa bile geçerlidir.
async function a() {
console.log('a başladı');
await b();
console.log('a devam etti');
}
async function b() {
console.log('b çalıştı');
}
setTimeout(() => console.log('setTimeout'), 0);
a();
Promise.resolve().then(() => console.log('then'));
console.log('senkron son');
// a başladı
// b çalıştı
// senkron son
// a devam etti
// then
// setTimeoutb() senkron olarak çalışır çünkü async fonksiyonun ilk await'e kadar olan kısmı senkron yürütülür. a'nın devamı, sonradan kaydedilen then'den önce kuyruğa girdiği için önce çalışır. Ayrıntılar için async/await rehberimize bakın.
Microtask kuyruğu task'ları geciktirebilir
Microtask kuyruğu, task kuyruğuna geçilmeden önce tamamen boşaltıldığı için kendini sürekli yeniden kuyruğa ekleyen microtask'lar zamanlayıcıları ve G/Ç geri çağrılarını geciktirir. Aşağıda 100.000 adımlık bir microtask zinciri, 0 ms'lik zamanlayıcıdan önce tamamlanır:
setTimeout(() => console.log('timeout'), 0);
function zincir(n) {
if (n === 0) return;
Promise.resolve().then(() => zincir(n - 1));
}
zincir(100000);
Promise.resolve().then(() => console.log('mikrogörevler'));
// mikrogörevler
// timeoutUzun işleri parçalara bölmek
MDN'in önerisi, işleri kısa tutmak ve mümkünse uzun bir işi birkaç işe bölmektir. Büyük bir listeyi işlerken her parçadan sonra kalan kısmı setTimeout ile yeni bir task olarak planlarsanız, arada bekleyen diğer task'lar da çalışma fırsatı bulur:
function parcala(liste, i = 0) {
const son = Math.min(i + 2, liste.length);
for (; i < son; i++) console.log('işlendi', liste[i]);
if (i < liste.length) setTimeout(() => parcala(liste, i), 0);
}
parcala(['a', 'b', 'c', 'd', 'e']);
setTimeout(() => console.log('araya giren görev'), 0);
// işlendi a
// işlendi b
// işlendi c
// işlendi d
// araya giren görev
// işlendi eNode.js tarafında CPU yoğun işler için olay döngüsünü bloklamamaya dair pratik öneriler Node.js performans optimizasyonu yazımızda yer alıyor.
JavaScript tek iş parçacıklı mı?
Bir agent içinde evet: aynı anda yalnızca bir iş yürütülür. Promise yöntemleriyle "eşzamanlı" görünen işler aslında sırayla, kontrolü birbirine devrederek ilerler. MDN'e göre gerçek paralel yürütme, her biri kendi agent'ına sahip olan worker'larla (tarayıcıda Web Worker, Node.js'te worker threads) mümkündür. Bu agent'lar belleği yalnızca SharedArrayBuffer ve Atomics gibi açık mekanizmalarla paylaşır.
Event loop'un bir başka garantisi de "asla bloklamama"dır: G/Ç genellikle olaylar ve callback'lerle yönetildiği için bir ağ isteği beklenirken uygulama diğer işleri yapmaya devam edebilir. MDN, alert() veya senkron XHR gibi eski istisnalardan kaçınılmasını önerir.
Sık yapılan hatalar

- setTimeout(fn, 0)'ın hemen çalışacağını sanmak: mevcut senkron kod ve tüm microtask'lar bittikten sonra çalışır.
- Senkron ağır döngüler: run-to-completion yüzünden diğer tüm işler bekler. İşi bölün veya worker'a taşıyın.
- Sonsuz microtask zinciri: task kuyruğunu aç bırakır; zamanlayıcılar ve G/Ç geri çağrıları gecikir.
- Sıralamayı tahmin etmek yerine ölçmemek: karmaşık sıralamalarda küçük bir betikle gerçek çıktıyı kontrol edin.
- Gereksiz await zincirleri: her
awaitek bir microtask demektir ve sıralamayı değiştirir.
Sık Sorulan Sorular
Microtask ile task arasındaki fark nedir?
Microtask'lar (Promise işleyicileri, await devamları, queueMicrotask) yüksek önceliklidir ve mevcut iş bittiği anda, bir sonraki task'tan önce hepsi çalıştırılır. Task'lar (zamanlayıcılar, olaylar, G/Ç) birer birer alınır ve her birinin ardından microtask kuyruğu yeniden boşaltılır.
Event loop ECMAScript spesifikasyonunda mı tanımlı?
Spesifikasyon job kavramını, Promise işlerinin kaydedildikleri sırayla çalışması gerektiğini ve bir job'un tamamlanmadan diğerinin başlayamayacağını tanımlar. Task ve microtask ayrımı ise HTML standardı gibi host tanımlarından gelir.
Promise.resolve().then() ile queueMicrotask() aynı mı?
İkisi de microtask kuyruğuna iş ekler ve MDN'e göre aynı öncelikte, kaydedilme sırasıyla çalışır. Yukarıdaki sıralama örneğinde de iki kaynaktan gelen işlerin birbirinin arasına girdiğini görebilirsiniz.
Generator'lar da yığından çıkıp geri döner mi?
Evet. MDN, generator'ın yürütme bağlamının yield'de askıya alınıp yığından çıkarıldığını, next() çağrıldığında yığına geri konup kaldığı yerden devam ettiğini anlatır. Ayrıntılar için generator ve iterator yazımıza bakın.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: JavaScript öğrenme rehberi · Promise nedir · async/await · Closure · Bellek yönetimi · Node.js performans