Chapter Bagaimana Node.js Bekerja telah memperkenalkan event loop sebagai penjadwal yang menjemput hasil operasi asynchronous setelah kode sinkron selesai. Pada chapter ini kita masuk lebih dalam ke mesinnya: event loop ternyata bukan satu antrean tunggal, melainkan siklus fase-fase yang dijalankan pustaka libuv dengan urutan yang tetap. Kita akan memetakan fase-fase tersebut, mengidentifikasi callback mana yang dieksekusi di tiap fase, dan menetapkan aturan urutan mana yang benar-benar terjamin — serta mana yang tidak.

Enam Fase di Dalam libuv #

Event loop diimplementasikan di dalam libuv dan berputar sebagai satu siklus tak hingga selama masih ada pekerjaan tertunda. Satu putaran penuh melewati enam fase, dan setiap fase memiliki antrean callback sendiri yang diproses secara FIFO (first in, first out):

Ringkasan tugas tiap fase dapat kita lihat pada tabel berikut:

FaseYang dieksekusiContoh
timersCallback setTimeout dan setInterval yang batas waktunya sudah terlampauisetTimeout(fn, 100) setelah 100 milidetik berlalu
pending callbacksCallback I/O yang ditunda ke putaran berikutnya serta beberapa error sistem tertentuError ECONNREFUSED pada koneksi TCP yang gagal
idle, preparePekerjaan internal libuvTidak relevan untuk kode JavaScript kita
pollMenjemput hasil operasi I/O yang telah selesai; menunggu event baru bila antrean kosongCallback fs.readFile, data masuk dari socket
checkCallback setImmediateTugas yang ingin berjalan tepat setelah fase poll
close callbacksEvent close pada handle yang ditutupsocket.on("close", ...)

Dua antrean penting justru berada di luar keenam fase ini: nextTick queue dan microtask queue. Keduanya dikosongkan setelah setiap callback selesai dieksekusi, sebelum event loop melangkah ke pekerjaan berikutnya. Karena itu callback process.nextTick() dan Promise selalu mendahului perpindahan antar fase — pembahasan tuntasnya ada pada Chapter Call Stack dan Microtask Queue serta Chapter process.nextTick dan setImmediate.

Aturan Urutan yang Tidak Bisa Diandalkan #

Ada satu pasangan yang sering membingungkan: setTimeout(fn, 0) versus setImmediate(fn) ketika keduanya didaftarkan langsung di level teratas module. Logika awam menyangka setTimeout(fn, 0) berjalan lebih dulu karena tampak “instan”. Kenyataannya, urutan keduanya tidak ditentukan:

js Contoh: urutan tidak menentu di level teratas
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));

Penyebabnya ada pada fase timers: batas waktu setTimeout(fn, 0) dibulatkan menjadi 1 milidetik, dan fase timers pada putaran pertama bisa saja dilewati sebelum 1 milidetik itu terlampaui, tergantung lamanya proses bootstrap Node.js dan pemuatan module. Bila batas waktu belum terlampaui saat fase timers diperiksa, callback setImmediate di fase check berjalan lebih dulu; bila sudah terlampaui, urutannya terbalik. Output program di atas bisa berbeda antar eksekusi — dan juga antar mesin.

Urutan yang Terjamin di Dalam Callback I/O #

Situasi berubah total ketika kedua penjadwalan tersebut didaftarkan dari dalam callback I/O. Dalam posisi itu, eksekusi sedang berada di fase poll, sehingga fase check pada putaran yang sama pasti dikunjungi lebih dulu sebelum siklus kembali ke fase timers. Artinya setImmediate dijamin mendahului setTimeout(fn, 0). Program berikut membuktikannya dengan membaca berkasnya sendiri:

js
import { readFile } from "node:fs";

console.log("1. Kode sinkron: mendaftarkan pembacaan berkas");

readFile(new URL("./urutan-fase.mjs", import.meta.url), () => {
  console.log("2. Callback I/O berjalan pada fase poll");
  setImmediate(() => {
    console.log("3. setImmediate dijemput pada fase check");
  });
  setTimeout(() => {
    console.log("4. setTimeout dijemput pada fase timers");
  }, 0);
});

Output yang Dihasilkan:

text
1. Kode sinkron: mendaftarkan pembacaan berkas
2. Callback I/O berjalan pada fase poll
3. setImmediate dijemput pada fase check
4. setTimeout dijemput pada fase timers

Mari kita telaah perjalanan kode di atas melalui fase-fase event loop:

  1. Baris 3: kode sinkron mencetak pesan pertama; pada titik ini event loop belum berputar sama sekali.
  2. Baris 5: readFile() mendaftarkan operasi baca berkas ke libuv, lalu eksekusi module selesai. Event loop mulai berputar.
  3. Baris 6: hasil pembacaan siap dijemput pada fase poll — pesan kedua tercetak. Di dalam callback inilah dua penjadwalan baru didaftarkan.
  4. Baris 7-9: setImmediate menaruh callback-nya di antrean fase check. Karena fase check dikunjungi tepat setelah poll pada putaran yang sama, pesan ketiga tercetak lebih dulu.
  5. Baris 10-12: setTimeout menunggu putaran berikutnya sampai fase timers mengunjunginya — pesan keempat tercetak paling akhir, dan setelah itu proses keluar karena tidak ada pekerjaan tersisa.

Kapan Event Loop Berhenti #

Event loop terus berputar hanya selama masih ada handle atau request yang aktif — misalnya timer yang belum berjalan, koneksi jaringan yang terbuka, atau operasi I/O yang belum selesai. Ketika semua pekerjaan selesai dan antrean kosong, libuv mengakhiri siklus dan proses Node keluar dengan sendirinya. Kita bisa mengubah kontribusi sebuah timer terhadap keputusan itu lewat method unref(), yang dibahas praktis pada Chapter Timer dan Scheduling. Untuk pendalaman resmi mengenai fase-fase event loop, panduan event loop, timers, dan process.nextTick serta dokumentasi timers adalah rujukan terbaik.

Kini peta fase sudah kita pegang: timers di awal putaran, poll sebagai pusat I/O, check untuk setImmediate. Pertanyaan berikutnya mengarah ke dalam satu callback itu sendiri — bagaimana V8 mengeksekusi kode di dalamnya, dan bagaimana microtask menyisip di sela-sela pekerjaan? Chapter Call Stack dan Microtask Queue membedah lapisan tersebut.