Event loop adalah putaran yang memilih kode mana yang dijalankan berikutnya, setiap kali JavaScript di Node selesai mengerjakan sesuatu. Bayangin kantor pos dengan satu petugas. Dia selalu menyelesaikan orang yang sedang di depannya dulu. Setelah itu dia menghabiskan tumpukan memo kecil yang mendesak, sampai kosong. Baru dia keliling ke baki-baki dengan urutan tetap: baki "alarm" (timer), baki "paket datang" (I/O), baki "setelah paket" (setImmediate), lalu baki "tutup". Setiap selesai satu urusan dari baki, tumpukan memo dihabiskan lagi. Memo itu namanya microtask.
🧪 Tebakan cepat: kode ini mencetak
"1 sync", menjadwalkan timer 0 ms yang mencetak"4 timer", menjadwalkanqueueMicrotaskyang mencetak"2 microtask", menjadwalkanPromise.resolve().thenyang mencetak"3 promise", lalu mencetak"1b sync". Apa urutan output-nya? Tebak dulu, jawabannya ada di bagian "Microtask dulu, baru timer".
Satu call stack, kode sinkron dulu
JavaScript di Node punya satu call stack: daftar function yang sedang jalan, cuma bisa mengerjakan satu hal dalam satu waktu. Kode sinkron, baris demi baris di file-mu, jalan sampai habis lebih dulu. Callback yang dijadwalkan (timer, Promise, I/O) gak pernah memotong di tengah. Mereka antre, dan event loop baru mengambilnya saat call stack kosong.
Microtask dulu, baru timer
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
A
D
C
B
A dan D sinkron, jadi duluan. .then masuk ke microtask queue, antrean yang berisi lanjutan Promise (.then, .catch, sisa function setelah await) dan queueMicrotask(fn). Microtask queue dihabiskan sampai kosong sebelum event loop pindah ke fase berikutnya, termasuk sebelum timer apa pun. Jadi C sebelum B, walau timernya 0 ms.
Di dalam microtask queue, urutannya sesuai urutan dijadwalkan (yang pertama masuk, pertama keluar). Untuk tebakannya: 1 sync dan 1b sync dulu karena sinkron, lalu 2 microtask (dijadwalkan duluan), 3 promise, dan terakhir 4 timer. Itu jawaban tebakannya.
Fase-fase event loop Node
Node memakai libuv, library bahasa C yang menjalankan event loop dan mengurus I/O. Satu putaran melewati fase-fase ini, berurutan:
| Fase | Yang dijalankan di situ |
|---|---|
| timers | callback setTimeout dan setInterval yang waktunya sudah lewat |
| pending callbacks | beberapa callback error sistem yang ditunda dari putaran sebelumnya |
| poll | callback I/O: file selesai dibaca, data atau koneksi baru datang. Kalau gak ada yang lain, Node menunggu di sini |
| check | callback setImmediate |
| close callbacks | callback penutupan, misalnya socket.on("close") |
Setiap selesai satu callback, Node menghabiskan antrean process.nextTick dulu, lalu microtask queue, baru lanjut. Pekerjaan berat seperti membaca file, dns.lookup, crypto.pbkdf2, dan kompresi zlib dikerjakan thread pool milik libuv (default 4 thread) di luar JavaScript, sedangkan koneksi jaringan ditunggu lewat notifikasi sistem operasi. JavaScript-mu sendiri tetap jalan di satu thread.
process.nextTick dan setImmediate
console.log("sync");
setImmediate(() => console.log("setImmediate"));
setTimeout(() => console.log("setTimeout 0"), 0);
Promise.resolve().then(() => console.log("promise"));
process.nextTick(() => console.log("nextTick"));
console.log("end sync");
Hasil di Node 24, file app.js biasa (CommonJS):
sync
end sync
nextTick
promise
setImmediate
setTimeout 0
process.nextTick(fn) punya antrean sendiri yang diproses sebelum microtask lain, jadi nextTick tercetak sebelum promise. Dua baris terakhir gak dijamin urutannya: di kode utama, setTimeout(fn, 0) baru siap kalau 1 ms sudah lewat saat putaran pertama dimulai. Kami jalankan 100 kali di Node 24: 99 kali setImmediate duluan, sekali setTimeout 0 duluan. Jangan bergantung pada urutan itu.
Di dalam callback I/O, urutannya pasti:
fs.readFile("app.js", () => {
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
});
Selalu immediate lalu timeout. Callback readFile jalan di fase poll, dan fase sesudah poll adalah check, tempat setImmediate. Timer baru dapat giliran di putaran berikutnya.
Memblokir event loop
const mulai = Date.now();
setTimeout(() => console.log("timer 10 ms jalan setelah " + (Date.now() - mulai) + " ms"), 10);
const t = Date.now();
while (Date.now() - t < 300) {} // kerja sinkron 300 ms
Output-nya timer 10 ms jalan setelah 301 ms. Selama loop sinkron itu jalan, event loop gak bisa berputar: timer, I/O, dan semua request lain menunggu. Hal yang sama terjadi kalau process.nextTick atau microtask terus menjadwalkan dirinya sendiri: antreannya gak pernah kosong, dan timer gak pernah dapat giliran (starvation). Kerja CPU yang berat dipindah ke Worker Threads, yang dibahas di pelajaran nanti.
Di latihan: buktikan urutannya
Kode awal sudah mencetak 1 sync dan 1b sync. Di antaranya, jadwalkan tiga hal, masing-masing mencetak dari dalam penjadwalnya sendiri: setTimeout(() => { console.log("4 timer"); }, 0);, lalu queueMicrotask(() => { console.log("2 microtask"); });, lalu Promise.resolve().then(() => { console.log("3 promise"); });. Output-nya harus persis lima baris, urut: 1 sync, 1b sync, 2 microtask, 3 promise, 4 timer.
⚠️ Jebakan yang sering ketemu
- Menganggap
setTimeout(fn, 0)langsung jalan. Dia menunggu semua kode sinkron dan semua microtask selesai. - Bergantung pada urutan
setTimeout 0dansetImmediatedi kode utama. Bisa keluar dua-duanya. Di dalam callback I/O,setImmediateselalu duluan. - Kerja sinkron berat di handler. Loop besar,
JSON.parsefile raksasa, atau fungsiSyncmembekukan seluruh server selama itu. process.nextTickatau microtask yang menjadwalkan dirinya sendiri terus-menerus. Timer dan I/O kelaparan.- Urutan
nextTickdanpromisedi ES Modules. Di file.mjs, kode utamanya sendiri berjalan sebagai microtask, jadipromisetercetak sebelumnextTick. Aturan "nextTick duluan" berlaku untuk callback dan file CommonJS.
TL;DR: JavaScript di Node punya satu call stack: kode sinkron jalan sampai habis dulu. Lalu antrean process.nextTick, lalu microtask queue (.then, await, queueMicrotask) dihabiskan sampai kosong, baru fase berikutnya: timers, pending callbacks, poll (I/O), check (setImmediate), close. Jadi A D C B, dan 1 sync, 1b sync, 2 microtask, 3 promise, 4 timer. process.nextTick jalan sebelum microtask lain. Urutan setTimeout 0 dan setImmediate di kode utama gak dijamin; di dalam callback I/O, setImmediate selalu duluan. libuv mengerjakan I/O dan thread pool-nya (4 thread) di luar JavaScript. Kode sinkron yang berat atau nextTick yang berulang terus memblokir event loop untuk semua request.