Pada chapter sebelumnya kita melihat setTimeout mendaftarkan callback lalu thread melanjutkan jalan. Pertanyaannya tinggal satu: siapa yang memanggil callback itu pada waktunya, sementara thread sedang sibuk mengerjakan kode lain? Jawabannya adalah event loop — mekanisme inti yang mengatur giliran eksekusi di JavaScript, baik di browser maupun di Node.js.
Event loop menjelaskan hampir semua kejanggalan asynchronous yang sering membingungkan: mengapa kode yang ditulis belakangan bisa berjalan lebih dulu, mengapa delay 0 tidak berarti seketika, dan mengapa handler promise mendahului timer. Chapter ini membedah tiga komponen utamanya — call stack, antrean macrotask, dan antrean microtask — lalu membuktikan urutannya dengan uji nyata di Node.
Call Stack dan Dua Antrean #
Runtime memegang tiga wadah yang bekerja bersama:
- Call stack — tumpukan yang mencatat function yang sedang berjalan. Ini satu-satunya tempat kode dieksekusi, dan isinya selalu satu tugas dalam satu waktu.
- Antrean macrotask — antrean pekerjaan utuh seperti callback
setTimeoutdansetInterval. Setiap entri menunggu giliran masuk ke call stack. - Antrean microtask — antrean tugas cepat yang terutama diisi handler promise (
then,catch,finally). Antrean ini diproses lebih dulu daripada macrotask.
Event loop adalah pengatur giliran yang menghubungkan ketiganya. Siklusnya bisa dirangkum dalam satu diagram:
Dua aturan pada diagram perlu digarisbawahi. Pertama, microtask dikosongkan setiap kali call stack kosong — bukan satu entri saja antara macrotask. Kedua, hanya satu macrotask yang diambil per siklus, lalu antrean microtask diperiksa lagi sebelum macrotask berikutnya.
Dari mana isi kedua antrean itu? Sumber macrotask yang akan kita temui di course ini: callback setTimeout dan setInterval (chapter Timers), event antarmuka seperti klik (section Browser Events), dan operasi I/O di Node. Sumber microtask yang paling sering: handler promise — then, catch, dan finally — plus queueMicrotask yang jarang dipakai langsung. Aturan prioritasnya sederhana untuk diingat: handler promise selalu mendahului timer.
Membuktikan Urutan dengan Uji Nyata #
Aturan diagram di atas kita buktikan dengan berkas kecil yang memadukan kode sinkron, microtask, dan macrotask dalam satu program:
// urutan-event-loop.js
// Bukti urutan: kode sinkron, lalu microtask, macrotask terakhir.
// --- 1. kode sinkron: langsung masuk call stack ---
console.log("1. kode sinkron pertama");
// --- 2. macrotask: callback didaftarkan ke antrean timer ---
setTimeout(() => {
console.log("4. macrotask: callback setTimeout");
}, 0);
// --- 3. microtask: handler didaftarkan ke antrean promise ---
Promise.resolve().then(() => {
console.log("3. microtask: handler Promise.then");
});
// --- 4. kode sinkron terakhir ---
console.log("2. kode sinkron terakhir");Output yang Dihasilkan:
1. kode sinkron pertama
2. kode sinkron terakhir
3. microtask: handler Promise.then
4. macrotask: callback setTimeoutMari kita telaah apa yang terjadi:
- Baris 4 (
console.log("1. kode sinkron pertama")): masuk call stack dan mencetak seketika; belum ada yang mengantri. - Baris 7–9 (
setTimeout): callback di dalamnya tidak dieksekusi di sini. Runtime mendaftarkannya ke antrean macrotask; angka0bukan perintah “jalankan sekarang”, melainkan “boleh mengantri setelah 0 milidetik”. - Baris 12–14 (
Promise.resolve().then): handler mendaftar ke antrean microtask. Promise yang sudah fulfilled menjadwalkan handler-nya seketika. - Baris 17 (
console.log("2. ...")): masih bagian dari kode sinkron. Call stack belum kosong, sehingga kedua antrean tetap menunggu. - Setelah baris 17: call stack kosong. Event loop mengosongkan seluruh antrean microtask terlebih dahulu — nomor 3 tercetak — lalu mengambil satu macrotask, sehingga nomor 4 tercetak paling akhir.
Karena nomor pada keluaran mengikuti urutan eksekusi nyata, urutan ini adalah bukti empiris siklus event loop, bukan sekadar teori.
Antrean Microtask Dikosongkan Sampai Tuntas #
Aturan “kosongkan semua microtask” punya konsekuensi menarik: microtask yang mendaftarkan microtask baru tetap habis sebelum macrotask mana pun diberi giliran. Jalankan cuplikan berikut:
// Microtask baru hasil microtask tetap diproses dalam siklus yang sama.
setTimeout(() => console.log("timeout jalan"), 0);
Promise.resolve().then(() => {
console.log("microtask 1");
Promise.resolve().then(() => console.log("microtask 2 (anak dari microtask 1)"));
});
console.log("kode sinkron");Output yang Dihasilkan:
kode sinkron
microtask 1
microtask 2 (anak dari microtask 1)
timeout jalanHandler pertama mencetak microtask 1, lalu mendaftarkan microtask 2. Karena antrean microtask harus kosong sebelum macrotask diberi giliran, microtask 2 dieksekusi lebih dulu daripada timer yang mengantri lebih awal. Runtime menyelesaikan seluruh antrean microtask dalam satu kesempatan, tanpa jeda di antara keduanya.
Mengapa runtime memilih mengosongkan antrean sampai tuntas, bukan satu per satu? Alasannya konsistensi. Microtask biasanya berisi tindak lanjut kecil dari satu promise — meneruskan nilai, menandai selesai, memicu handler berikutnya. Menyelesaikan semuanya sebelum menyentuh macrotask menjamin satu hal yang penting: ketika callback timer berjalan, seluruh rantai promise yang sudah tertunda sudah beres, sehingga state program tidak berubah di tengah-tengah pekerjaan timer.
Dua Pertanyaan yang Sering Muncul #
“Mengapa handler promise lebih dulu daripada setTimeout dengan delay 0?” Karena keduanya mengantri di tempat berbeda. Handler promise masuk ke antrean microtask yang dikosongkan segera setelah call stack kosong, sedangkan timer masuk ke antrean macrotask yang baru diproses setelah antrean microtask habis. Keluaran berkas utama di atas adalah buktinya.
“Apakah antrean bisa menyela kode yang sedang berjalan?” Tidak bisa. Selama satu function berjalan di call stack, tidak ada callback dari antrean mana pun yang boleh masuk. Inilah yang membuat satu function sinkron selalu aman menyelesaikan pekerjaannya — dan sekaligus alasan mengapa function yang berjalan lama menyulitkan seluruh program, seperti dibuktikan di chapter sebelumnya.
Ringkasan Aturan Event Loop #
Empat aturan berikut cukup untuk menjawab hampir semua soal urutan eksekusi:
- Kode sinkron berjalan sampai call stack kosong; tidak ada antrean yang bisa menyela.
- Setelah call stack kosong, seluruh microtask diproses sampai tuntas, termasuk microtask baru hasil microtask.
- Satu macrotask diambil dan dijalankan, lalu siklus kembali ke aturan kedua.
- Delay
setTimeoutadalah waktu minimal sebelum callback boleh mengantri, bukan jaminan eksekusi tepat pada waktunya.
Aturan-aturan ini bukan sekadar teori untuk ujian. Ketika keluaran program tidak sesuai harapan, urutan antrean adalah penyebab paling sering: log yang tercetak terbalik, handler yang jalan setelah timer, atau update tampilan yang terasa terlambat. Dengan keempat aturan di atas, seluruh kejadian itu bisa dilacak ke sumbernya.
Dengan urutan ini terasa sekali bahwa callback yang dititipkan ke runtime adalah pola paling dasar kode asinkron. Chapter berikutnya merapikan pola tersebut menjadi konvensi yang seragam: callback error-first ala Node.js.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
urutan-event-loop.js
Unduh berkas// urutan-event-loop.js
// Bukti urutan: kode sinkron, lalu microtask, macrotask terakhir.
// --- 1. kode sinkron: langsung masuk call stack ---
console.log("1. kode sinkron pertama");
// --- 2. macrotask: callback didaftarkan ke antrean timer ---
setTimeout(() => {
console.log("4. macrotask: callback setTimeout");
}, 0);
// --- 3. microtask: handler didaftarkan ke antrean promise ---
Promise.resolve().then(() => {
console.log("3. microtask: handler Promise.then");
});
// --- 4. kode sinkron terakhir ---
console.log("2. kode sinkron terakhir");