Node.js'te process.nextTick(), verilen fonksiyonu o anda çalışan JavaScript işlemi biter bitmez, event loop bir sonraki faza geçmeden önce çalıştırır. setImmediate() ise fonksiyonu event loop'un check fazına, yani I/O olaylarını işleyen poll fazından hemen sonraya koyar. setTimeout(fn, 0) da timers fazında, en az 1 ms'lik eşik dolduktan sonra çalışır. Kısacası sıra genellikle şöyledir: senkron kod, nextTick kuyruğu, Promise mikro görevleri, ardından zamanlayıcılar ve setImmediate; ancak son ikisinin sırası nereden çağrıldıklarına bağlıdır.

Bu yazı yalnızca Node.js'e özgü kısmı anlatır: libuv tabanlı event loop fazları, nextTick kuyruğu, CommonJS ile ES modüllerinde sıranın neden farklılaştığı ve hangi durumda hangisini seçmeniz gerektiği. Çağrı yığını, görev kuyruğu ve mikro görev kavramlarının dil düzeyindeki anlatımı için JavaScript event loop yazısına, serinin genel haritası için Node.js öğrenme rehberi sayfasına bakabilirsiniz. Örnekler Node.js v26.13.0 ile çalıştırıldı.

Node.js event loop fazları

Node.js event loop fazları: timers, pending callbacks, idle, prepare, poll

Node.js başladığında event loop'u hazırlar, verdiğiniz betiği çalıştırır (betik zamanlayıcı kurabilir, asenkron API çağırabilir, process.nextTick() kullanabilir) ve ardından döngüyü işletmeye başlar. Learn rehberindeki basitleştirilmiş şemaya göre her turda şu fazlar sırayla dolaşılır. Her fazın kendi FIFO (ilk giren ilk çıkar) geri çağırma kuyruğu vardır; döngü bir faza girdiğinde o fazın kuyruğu boşalana ya da sistemin belirlediği üst sınıra ulaşılana kadar geri çağırmaları çalıştırır, sonra bir sonraki faza geçer.

FazNe çalışır
timerssetTimeout ve setInterval geri çağırmaları
pending callbacksBir sonraki tura ertelenmiş bazı sistem I/O geri çağırmaları (ör. bazı TCP hataları)
idle, prepareYalnızca Node.js içinde kullanılır
pollYeni I/O olaylarını alır ve çoğu I/O geri çağırmasını çalıştırır; uygun olduğunda burada bekler
checksetImmediate geri çağırmaları
close callbacksBazı kapanış geri çağırmaları, ör. socket.on('close')

Rehber iki ayrıntının altını çizer. Birincisi, libuv 1.45.0 ile (Node.js 20) zamanlayıcılar her turda poll fazından sonra çalıştırılmaya başlandı; geriye dönük uyumluluk için döngüye girmeden önce de bir kez çalıştırılıyorlar. İkincisi, Windows ile Unix/Linux uygulamaları arasında küçük farklar vardır ama yukarıdaki fazlar Node.js'in fiilen kullandığı kısımdır. Her tur arasında Node.js bekleyen bir I/O ya da zamanlayıcı olup olmadığına bakar; yoksa süreç temiz biçimde kapanır.

Hepsi bir arada: çalışma sırası (CommonJS)

Aşağıdaki betik beş farklı erteleme yöntemini aynı anda kullanıyor. .cjs uzantısıyla, yani CommonJS modülü olarak çalıştırıldığında çıktı şöyledir:

javascript
// sira.cjs
setTimeout(() => console.log('setTimeout 0'), 0);
setImmediate(() => console.log('setImmediate'));
Promise.resolve().then(() => console.log('promise.then'));
queueMicrotask(() => console.log('queueMicrotask'));
process.nextTick(() => console.log('nextTick'));
console.log('senkron kod');
// çıktı: senkron kod
// çıktı: nextTick
// çıktı: promise.then
// çıktı: queueMicrotask
// çıktı: setTimeout 0
// çıktı: setImmediate

Önce senkron kod biter. Sonra Node.js nextTick kuyruğunu tamamen boşaltır ve hemen ardından Promise mikro görev kuyruğunu (then ve queueMicrotask) işler. Belgelere göre nextTick kuyruğu her boşaltıldığında mikro görev kuyruğu da hemen arkasından boşaltılır. Son iki satırın sırası ise garanti değildir; bunu aşağıda ayrıca ele alıyoruz.

ES modüllerinde sıra neden değişir?

Aynı kodu .mjs dosyası olarak çalıştırdığınızda Promise işleyicileri nextTick'in önüne geçer:

javascript
// sira.mjs (aynı kod, ES modülü)
setTimeout(() => console.log('setTimeout 0'), 0);
setImmediate(() => console.log('setImmediate'));
Promise.resolve().then(() => console.log('promise.then'));
queueMicrotask(() => console.log('queueMicrotask'));
process.nextTick(() => console.log('nextTick'));
console.log('senkron kod');
// çıktı: senkron kod
// çıktı: promise.then
// çıktı: queueMicrotask
// çıktı: nextTick
// çıktı: setTimeout 0
// çıktı: setImmediate

Belgelerin açıklaması şudur: ES modülü asenkron bir işlem olarak yüklenir, yani betiğin tamamı zaten mikro görev kuyruğunun işlenmesi sırasında çalışmaktadır. Node.js başka bir kuyruğa geçmeden önce mikro görev kuyruğunu bitirmeye çalıştığı için yeni eklenen then ve queueMicrotask işleri, nextTick kuyruğundan önce çalışır. Bu fark, kodu CommonJS'ten ESM'e taşırken sıralamaya dayanan mantığın sessizce değişebileceği anlamına gelir. Modül sistemleri arasındaki diğer farklar için CommonJS ve ESM yazısına göz atın.

setTimeout(fn, 0) mı, setImmediate() mı önce çalışır?

setTimeout(fn, 0) mı, setImmediate() mı önce çalışır?

Learn rehberine göre ikisi de ana modülden (bir I/O döngüsünün dışından) çağrıldığında sıra belirsizdir ve sürecin o anki performansına bağlıdır. Aşağıdaki iki satırlık betiği aynı makinede 15 kez çalıştırdık; 13 seferde immediate, 2 seferde timeout önce geldi:

javascript
// timeout-immediate.cjs
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// çıktı (çoğu çalıştırmada): immediate
// çıktı (çoğu çalıştırmada): timeout
// bazı çalıştırmalarda sıra tersine döner: timeout, immediate

İki çağrıyı bir I/O geri çağırmasının içine taşıdığınızda ise sonuç her zaman aynıdır: setImmediate önce çalışır. Bunun nedeni, I/O geri çağırmasının poll fazında çalışması ve döngünün poll'den sonra doğrudan check fazına geçmesidir; zamanlayıcılar ancak bir sonraki dolaşmada sıraya girer. Rehber bunu setImmediate()'in asıl avantajı olarak sunar: I/O döngüsü içinde planlandığında, kaç zamanlayıcı olursa olsun hepsinden önce çalışır.

javascript
import { readFile } from 'node:fs';

readFile(import.meta.filename, () => {
  setTimeout(() => console.log('timeout'), 0);
  setImmediate(() => console.log('immediate'));
  process.nextTick(() => console.log('nextTick (aynı fazda, önce)'));
});
// çıktı: nextTick (aynı fazda, önce)
// çıktı: immediate
// çıktı: timeout

process.nextTick() nasıl çalışır?

process.nextTick() şemada yer almaz, çünkü teknik olarak event loop'un bir parçası değildir. Belgelere göre nextTick kuyruğu, JavaScript yığınındaki mevcut işlem tamamlandıktan sonra ve event loop devam etmeden önce tamamen boşaltılır. Burada "işlem", alttaki C/C++ işleyiciden çalıştırılması gereken JavaScript'e geçiş anlamına gelir. Yani hangi fazda çağırırsanız çağırın, nextTick geri çağırmaları o faz bir sonraki adıma geçmeden çalışır.

Bu güç bir risk de taşır: geri çağırma içinden yeniden process.nextTick() çağırırsanız kuyruk hiç boşalmaz ve event loop poll fazına ulaşamaz. Belgeler bunu I/O'nun "aç bırakılması" (starvation) olarak adlandırır ve özyinelemeli nextTick ile sonsuz döngü kurulabileceğini belirtir. Aşağıdaki örnekte 100.000 özyinelemeli nextTick çağrısının tamamı, önceden kurulmuş setImmediate'ten önce bitiyor:

javascript
let sayac = 0;

setImmediate(() => {
  console.log('setImmediate çalıştı, o ana kadar nextTick sayısı:', sayac);
});

function tekrar() {
  if (++sayac < 100000) process.nextTick(tekrar);
}
process.nextTick(tekrar);
// çıktı: setImmediate çalıştı, o ana kadar nextTick sayısı: 100000

Sayaç burada bir sınırda durduğu için betik sonlanıyor. Sınır olmasaydı setImmediate hiç çalışmaz, sunucu gelen bağlantıları işleyemezdi. Özyinelemenin yığını büyütmediğine de dikkat edin: rehbere göre her nextTick geri çağırması yığın boşaldıktan sonra çalıştığı için RangeError: Maximum call stack size exceeded hatası alınmaz; sorun yığın değil, döngünün kilitlenmesidir. Event loop'u bloklayan işlerin genel analizi için Node.js performans yazısına bakabilirsiniz.

nextTick neden var: her zaman asenkron API

Belgelerin üzerinde durduğu tasarım ilkesi şudur: bir API ya her zaman senkron ya da her zaman asenkron olmalıdır. Bazen hemen, bazen sonra geri çağıran bir fonksiyon, çağıran kodun hangi satırın önce çalışacağını bilememesine yol açar. Örneğin argümanı doğrulayan ve hatalıysa callback'i hemen çağıran bir fonksiyon, geçerli argümanda ise gerçekten asenkron bir iş yapıyorsa, iki yolda farklı davranır. Hata yolunu da nextTick ile ertelemek tutarlılığı sağlar. process.nextTick() callback'ten sonraki argümanları geri çağırmaya iletir, bu yüzden ek bir sarmalayıcı fonksiyon gerekmez:

javascript
function kullaniciGetir(id, cb) {
  if (typeof id !== 'number') {
    return process.nextTick(cb, new TypeError('id sayı olmalı'));
  }
  setTimeout(() => cb(null, { id, ad: 'Ayşe' }), 5);
}

kullaniciGetir('abc', (err) => console.log('callback:', err.message));
console.log('çağrıdan sonraki satır');
// çıktı: çağrıdan sonraki satır
// çıktı: callback: id sayı olmalı

Hata yolunda bile callback, çağıran kodun geri kalanı çalıştıktan sonra tetiklendi. Callback tabanlı API'lerin genel yapısı için callback fonksiyon yazısına, bu tür API'leri Promise'e çevirmek için util.promisify yazısına bakabilirsiniz.

İkinci klasik kullanım, yapıcı içinden olay yaymaktır. Yapıcı çalışırken nesneyi oluşturan kod henüz on() çağıramamıştır; bu yüzden yapıcıda doğrudan yayılan olay kaybolur. Olayı nextTick ile ertelemek, dinleyicinin önce eklenmesine fırsat verir:

javascript
const { EventEmitter } = require('node:events');

class Baglanti extends EventEmitter {
  constructor() {
    super();
    this.emit('hazir');                          // dinleyici yok, kaybolur
    process.nextTick(() => this.emit('hazir'));  // yapıcı bitince yayılır
  }
}

const b = new Baglanti();
b.on('hazir', () => console.log('hazir olayı yakalandı'));
// çıktı: hazir olayı yakalandı

Çıktıda mesaj yalnızca bir kez görünüyor; o da ertelenen yayından geliyor. Rehber aynı nedenle net sunucusunun 'listening' olayının da nextTick ile kuyruğa alındığını anlatır: yalnızca port verildiğinde bağlama hemen gerçekleşir ve olay ertelenmeseydi server.on('listening') dinleyicisi henüz eklenmemiş olurdu. EventEmitter'ın ayrıntıları için EventEmitter yazısına bakın.

queueMicrotask() ile process.nextTick() farkı

queueMicrotask() ile process.nextTick() farkı: process.nextTick, queueMicrotask, Promise.then, setImmediate

queueMicrotask(), işi ayrı bir nextTick kuyruğu yerine çözülmüş Promise'lerin then, catch ve finally işleyicilerinin kullandığı mikro görev kuyruğuna koyar. Belgeler, çoğu kullanım için taşınabilir ve güvenilir olduğu gerekçesiyle queueMicrotask()'i önerir; basit durumlarda doğrudan yerine geçebilir. İki fark akılda tutulmalıdır: nextTick ek argümanları geri çağırmaya iletebilir, queueMicrotask() için ise closure ya da bind gerekir. Ayrıca CommonJS'te nextTick her zaman önce, ESM'de ise queueMicrotask önce çalışır; yukarıdaki iki çıktı bunu gösteriyor. Mikro görevde fırlatılan hatalar mümkünse geri çağırmanın içinde yakalanmalı, yakalanmazsa process.on('uncaughtException') ile ele alınabilir.

YöntemKuyruk / fazNe zaman çalışırEk argüman
process.nextTicknextTick kuyruğuMevcut işlem biter bitmez, faz değişmedenEvet
queueMicrotaskMikro görev kuyruğunextTick kuyruğundan hemen sonra (CJS), önce (ESM)Hayır
Promise.thenMikro görev kuyruğuqueueMicrotask ile aynı kuyruk, eklenme sırasıylaHayır
setImmediatecheck fazıPoll fazından sonra, döngünün o turundaEvet
setTimeout(fn, 0)timers fazıEn az 1 ms eşik dolduktan sonraki ilk uygun andaEvet

setImmediate() içinden setImmediate() çağırmak

Belgelere göre her döngü turunda setImmediate kuyruğunun tamamı işlenir; ancak çalışan bir immediate geri çağırmasının içinden kurulan yeni immediate bir sonraki tura kalır. Ayrıca her geri çağırmadan sonra nextTick kuyruğu yine boşaltılır. Aşağıdaki çıktı bu üç kuralı birlikte gösteriyor:

javascript
setImmediate(() => {
  console.log('immediate 1');
  setImmediate(() => console.log('iç immediate (sonraki tur)'));
  process.nextTick(() => console.log('immediate 1 içindeki nextTick'));
});
setImmediate(() => console.log('immediate 2'));
// çıktı: immediate 1
// çıktı: immediate 1 içindeki nextTick
// çıktı: immediate 2
// çıktı: iç immediate (sonraki tur)

Bu davranış setImmediate'i, uzun bir işi parçalara bölüp araya I/O girmesine izin vermek için nextTick'ten daha güvenli kılar: her parça yeni bir tura kaldığından, aradaki poll fazında bekleyen bağlantılar ve dosya işlemleri işlenebilir.

Hangisini ne zaman kullanmalı?

Learn rehberinin önerisi nettir: anlaşılması daha kolay olduğu için genel olarak setImmediate() kullanın. İsimler kafa karıştırıcıdır; rehber, process.nextTick()'in aslında setImmediate()'ten daha "hemen" çalıştığını, isimlerin yer değiştirmesi gerektiğini ama bunun npm'deki çok sayıda paketi bozacağı için yapılmayacağını söyler. Pratik bir özet:

  • Bir işi I/O'ya izin verecek şekilde ertelemek: setImmediate().
  • Mevcut işlem biter bitmez, döngü devam etmeden küçük bir iş yapmak: queueMicrotask(); ek argüman gibi özel bir ihtiyaç varsa process.nextTick().
  • Belirli bir süre sonra çalıştırmak: setTimeout(); Promise ile beklemek için node:timers/promises.
  • Callback'i her koşulda asenkron yapmak: hata yolunda da process.nextTick(cb, err) veya queueMicrotask().

Sık yapılan hatalar

  • setTimeout(fn, 0) ile setImmediate() sırasına güvenmek: Ana modülde sıra belirsizdir. Sıra önemliyse ikisini karıştırmayın ya da çağrıları bir I/O geri çağırmasının içine alın.
  • Özyinelemeli nextTick ile döngüyü kilitlemek: Büyük bir işi parçalarken her parçayı nextTick ile kurmak I/O'yu aç bırakır. Parçalar arasında setImmediate() kullanın.
  • CJS'ten ESM'e geçince sıranın aynı kalacağını sanmak: nextTick ile Promise işleyicilerinin göreli sırası modül türüne göre değişir.
  • Zamanlayıcı süresini kesin süre sanmak: Süre bir alt eşiktir; uzun süren geri çağırmalar ve poll fazı zamanlayıcıyı geciktirebilir.
  • Yapıcıdan doğrudan olay yaymak: Dinleyici henüz eklenmemiştir; olayı nextTick ile erteleyin ya da olayı yapıcı dışındaki bir metottan yayın.

Sık Sorulan Sorular

process.nextTick() kullanımdan kaldırıldı mı?

Hayır, kaldırılmadı; Node.js v26 belgelerinde Legacy olarak işaretli. Legacy, API'nin çalışmaya devam ettiği ama yeni kod için önerilmediği anlamına gelir. Belgeler, nextTick'in özel yeteneklerine ihtiyacınız yoksa queueMicrotask() kullanmanızı söyler.

setImmediate ile setTimeout 0 aynı şey mi?

Değil. setImmediate check fazında, setTimeout timers fazında çalışır. Ana modülden çağrıldıklarında sıraları belirsizdir; bir I/O geri çağırması içinden çağrıldıklarında setImmediate her zaman önce çalışır.

nextTick mi Promise.then mi önce çalışır?

Modül türüne bağlıdır. CommonJS dosyasında önce nextTick, sonra then çalışır. ES modülünde betik zaten mikro görev kuyruğu işlenirken çalıştığı için önce then ve queueMicrotask, sonra nextTick gelir.

Event loop fazları Windows'ta farklı mı?

Learn rehberi Windows ile Unix/Linux uygulamaları arasında küçük bir fark olduğunu, ancak anlatılan fazların her iki tarafta da Node.js'in kullandığı kısım olduğunu belirtir. Yazıdaki örneklerin çıktıları Windows üzerinde alındı; ana modüldeki setTimeout/setImmediate sırası her platformda belirsizdir.

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: Node.js öğrenme rehberi · JavaScript event loop · Node.js timers: setTimeout ve setInterval · EventEmitter kullanımı · Node.js performans optimizasyonu