Kita telah mengenal microtask dan fase-fase event loop secara terpisah; sekarang waktunya menyandingkan seluruh alat penundaan eksekusi dalam satu eksperimen. Node.js menyediakan process.nextTick(), setImmediate(), dan setTimeout(fn, 0) — tiga API yang sekilas tampak saling menggantikan, padahal ketiganya menempati lapisan penjadwalan yang berbeda. Chapter ini memetakan lapisan-lapisan tersebut lewat program yang urutan output-nya kita verifikasi langsung, termasuk satu kejutan pada level teratas module ESM yang sering membuat kode dari tutorial lama terlihat saling bertentangan.
Tiga Lapis Penjadwalan #
Pahami dulu di mana tiap API berada di dalam arsitektur runtime:
process.nextTick(callback)menaruh callback ke nextTick queue milik Node.js. Antrean ini dikosongkan tepat setelah callback yang sedang berjalan selesai — sebelum microtask queue, sebelum fase apa pun dari event loop berlanjut.Promise.then(callback)danqueueMicrotask(callback)menempati microtask queue milik V8 — satu lapis di bawah nextTick queue.setImmediate(callback)menaruh callback ke fase check yang dikunjungi setelah fase poll pada putaran event loop yang sama;setTimeout(callback, 0)menunggu fase timers pada putaran berikutnya.
Karena posisinya yang paling dekat dengan kode yang sedang berjalan, process.nextTick() adalah janji “segera setelah baris ini, sebelum apa pun yang lain”. Sementara setImmediate() adalah “segera setelah fase I/O selesai” — keduanya bukan “segera sekarang”, karena kode sinkron dan antrean yang lebih tinggi tetap didahulukan.
Urutan yang Terjamin di Dalam Callback I/O #
Eksperimen kunci chapter ini mendaftarkan keempat API sekaligus di dalam callback I/O — posisi di mana seluruh aturan prioritas berlaku penuh:
import { readFile } from "node:fs";
readFile(new URL("./urutan-io.mjs", import.meta.url), () => {
setTimeout(() => console.log("4. setTimeout(0) : fase timers"), 0);
setImmediate(() => console.log("3. setImmediate : fase check"));
Promise.resolve().then(() => console.log("2. Promise.then : microtask queue"));
process.nextTick(() => console.log("1. process.nextTick: nextTick queue"));
});Output yang Dihasilkan:
1. process.nextTick: nextTick queue
2. Promise.then : microtask queue
3. setImmediate : fase check
4. setTimeout(0) : fase timersUrutan tercetak berlawanan dengan urutan pendaftaran di kode, dan itulah yang terjamin:
process.nextTicklebih dulu: begitu callbackreadFileselesai, nextTick queue dikosongkan sebelum microtask queue sempat tersentuh.Promise.thenmenyusul: microtask queue dikosongkan seluruhnya sebelum event loop melanjutkan putarannya.setImmediateberikutnya: eksekusi sedang berada di fase poll, sehingga fase check dikunjungi pada putaran yang sama.setTimeout(0)terakhir: fase timers baru dikunjungi lagi pada putaran berikutnya.
Urutan empat lapis ini stabil dan dapat diandalkan selama pendaftaran dilakukan dari dalam callback event loop — baik callback I/O, timer, maupun setImmediate itu sendiri.
Kejutan di Level Teratas Module ESM #
Situasi menjadi tidak terduga ketika keempat callback didaftarkan langsung di baris teratas module. Bandingkan dua program berikut — isinya identik, hanya berbeda format module:
process.nextTick(() => console.log("2. process.nextTick"));
Promise.resolve().then(() => console.log("1. Promise.then"));
setTimeout(() => console.log("3. setTimeout(0)"), 0);Output yang Dihasilkan:
1. Promise.then
2. process.nextTick
3. setTimeout(0)process.nextTick(() => console.log("1. process.nextTick"));
Promise.resolve().then(() => console.log("2. Promise.then"));
setTimeout(() => console.log("3. setTimeout(0)"), 0);Output yang Dihasilkan:
1. process.nextTick
2. Promise.then
3. setTimeout(0)Pada module CommonJS, urutannya mengikuti aturan klasik: nextTick queue dulu, baru microtask queue. Pada module ESM, urutan dua lapis pertama justru terbalik: Promise.then mendahului process.nextTick. Penyebabnya ada pada cara Node.js memuat module: evaluasi ESM berjalan melalui mekanisme promise di V8, dan begitu evaluasi selesai, V8 otomatis mengosongkan microtask queue pada titik pemeriksaan (checkpoint) miliknya — sebelum Node.js sempat mengosongkan nextTick queue. Perhatikan juga bahwa pada kedua program di atas, keduanya masih mendahului setTimeout(0): urutan fase timers selalu paling akhir.
Bahaya nextTick Berantai #
Kekuatan “didahulukan sebelum segalanya” membawa konsekuensi serius: nextTick queue harus benar-benar kosong sebelum event loop boleh melanjutkan. Callback nextTick yang mendaftarkan callback nextTick lagi akan membuat antrean tidak pernah habis, dan seluruh event loop terhenti tanpa pernah mencapai fase mana pun:
function ulang() {
process.nextTick(ulang);
}
process.nextTick(ulang);
setTimeout(() => console.log("tidak pernah tercetak"), 0);Program di atas akan berjalan tanpa henti tanpa pernah mencetak apa pun, karena callback setTimeout menunggu fase timers yang tidak pernah dikunjungi — event loop terhenti memproses nextTick queue yang terus terisi ulang. Pola ini menelan seluruh kapasitas CPU untuk satu pekerjaan rekursif.
Ringkasan Empat Alat Penundaan #
| Alat | Antrean | Dijalankan | Kapan dipakai |
|---|---|---|---|
process.nextTick(fn) | nextTick queue (Node.js) | Tepat setelah callback saat ini, sebelum microtask | Kasus khusus: misalnya memastikan error dilempar setelah stack terlepas |
Promise.then(fn) / queueMicrotask(fn) | microtask queue (V8) | Setelah nextTick queue kosong | Hasil asynchronous, penyisipan ringan antar tugas |
setImmediate(fn) | fase check | Setelah fase poll pada putaran berjalan | Memberi jeda bagi I/O; urutan stabil di dalam callback I/O |
setTimeout(fn, 0) | fase timers (jeda dibulatkan ke 1 ms) | Putaran berikutnya setelah batas waktu | Penjadwalan berbasis waktu |
Rincian resmi ketiganya tersedia pada panduan event loop, timers, dan process.nextTick
, process.nextTick(callback)
, dan setImmediate(callback)
. Dari sini, jeda berbasis waktu yang lebih panjang dan berulang menjadi topik berikutnya: Chapter Timer dan Scheduling membahas setTimeout, setInterval, pembersihannya, serta cara membuat timer tidak menahan proses tetap hidup.