Exit code melaporkan akhir yang kita rencanakan sendiri — tetapi proses juga bisa diinterupsi dari luar: pengguna menekan Ctrl+C karena program terlalu lama berjalan, atau sistem pengelola mengirim perintah penghentian saat server dimatikan. Interupsi semacam ini dikirim dalam bentuk signal: pesan singkat dari sistem operasi yang bisa kita tangkap dan tanggani sebelum proses berakhir. Pada chapter ini kita menangkap SIGINT, membiarkan proses tetap hidup setelah interupsi diterima, mengamati perilaku bawaan tanpa handler, dan menutup dengan event exit sebagai tempat pembersihan terakhir.
Sinyal: Pemberitahuan dari Luar #
Signal adalah mekanisme lintas proses paling tua di sistem operasi ala Unix. Ketika shell, terminal, atau program lain mengirim sinyal ke proses kita, Node.js menerjemahkannya menjadi event pada object process — asalkan kita memasang listener-nya. Tanpa listener, sinyal dijawab dengan perilaku bawaan sistem operasi. Dua sinyal paling penting untuk aplikasi Node.js:
| Sinyal | Pemicu umum | Perilaku bawaan tanpa handler |
|---|---|---|
SIGINT | penekanan Ctrl+C di terminal | proses berakhir saat itu juga |
SIGTERM | perintah kill, penghentian servis oleh sistem | proses berakhir saat itu juga |
Keduanya bisa ditangkap, dan itulah gunanya: server menyimpan data, menutup koneksi database, atau menyelesaikan permintaan yang sedang berjalan sebelum benar-benar berhenti. Untuk menguji handler tanpa bergantung pada tombol terminal, kita bisa mengirim sinyal sendiri lewat process.kill(pid, sinyal) — dengan process.pid milik proses kita sendiri sebagai sasaran.
Menangkap SIGINT #
Berkas tangkap-sigint.mjs memasang handler SIGINT yang menyimpan data lalu keluar, ditambah listener exit yang mencatat kode keluar terakhir:
process.on("SIGINT", () => {
console.log("SIGINT diterima - menyimpan data lalu keluar");
process.exit(130);
});
process.on("exit", (kode) => {
console.log(`pembersihan akhir sebelum keluar dengan kode ${kode}`);
});
console.log("Program berjalan. Tekan Ctrl+C untuk menghentikan.");
setInterval(() => {}, 1000);
process.kill(process.pid, "SIGINT");Output yang Dihasilkan:
Program berjalan. Tekan Ctrl+C untuk menghentikan.
SIGINT diterima - menyimpan data lalu keluar
pembersihan akhir sebelum keluar dengan kode 130Mari kita telaah baris-per-baris kode di atas:
- Baris 1-4 (
process.on("SIGINT", ...)): handler dipasang paling awal, sebelum pekerjaan apa pun dimulai — kebiasaan penting agar tidak ada celah waktu ketika sinyal datang sebelum handler siap. Di dalamnya,process.exit(130)mengakhiri proses secara terkendali setelah pekerjaan penyelamatan selesai. - Baris 6-8 (
process.on("exit", ...)): eventexitterpicu tepat sebelum proses benar-benar berakhir, apa pun jalur akhirnya — termasuk dari dalamprocess.exit(). Inilah tempat pembersihan terakhir seperti menutup file handle. - Baris 11 (
setInterval(...)): interval kosong ini mewakili pekerjaan latar yang menjaga event loop tetap hidup — pada server sungguhan, peran ini diisi koneksi yang sedang dilayani. Callback sinyal hanya sempat berjalan bila event loop masih hidup. - Baris 13 (
process.kill(...)): baris terakhir mengirimSIGINTke proses sendiri — padanan deterministik dari menekanCtrl+C, sehingga contoh ini bisa diuji berulang kali tanpa terminal interaktif.
Handler Mencegah Akhir yang Mendadak #
Begitu handler terpasang, perilaku bawaan sinyal tidak lagi berlaku — proses hidup terus setelah SIGINT diterima, dan keputusan sepenuhnya milik handler kita. Berkas lanjut-kerja.mjs memilih untuk tidak keluar sama sekali:
let pengegar;
process.on("SIGINT", () => {
console.log("SIGINT diterima - pekerjaan diselesaikan lebih dulu");
clearInterval(pengegar);
});
console.log("mulai pekerjaan");
pengegar = setInterval(() => {}, 1000);
process.kill(process.pid, "SIGINT");Output yang Dihasilkan:
mulai pekerjaan
SIGINT diterima - pekerjaan diselesaikan lebih duluAlur programnya: pesan mulai pekerjaan tercetak, sinyal tiba, handler berjalan dan mencetak pesannya, lalu clearInterval() menghapus satu-satunya pekerjaan event loop — barulah proses berakhir secara alami dengan kode 0. Dua fakta lifecycle terlihat di sini. Pertama, handler yang tidak memanggil process.exit() membuat proses kebal terhadap Ctrl+C selama event loop masih punya pekerjaan; program interaktif memakai ini untuk menanyakan konfirmasi, dan program batch memakainya untuk menuntaskan operasi kritis. Kedua, begitu event loop kosong, proses tetap berakhir dengan sendirinya — tidak ada yang menggantung tanpa sebab.
Perilaku Bawaan Tanpa Handler #
Sebaliknya, proses tanpa handler langsung berakhir ketika sinyal tiba. Berkas mati-sinyal.mjs sengaja tidak memasang handler apa pun:
console.log("menunggu pekerjaan...");
setInterval(() => {}, 1000);
process.kill(process.pid, "SIGINT");Periksalah kode keluarnya dari shell:
node mati-sinyal.mjs
echo kode: $?Output yang Dihasilkan:
menunggu pekerjaan...
kode: 130Tidak ada pesan perpisahan — hanya menunggu pekerjaan... lalu kode 130 yang dilaporkan shell sebagai 128 + 2. Pola yang sama berlaku untuk SIGTERM: tanpa handler, proses berakhir dengan kode 143 (128 + 15), dan inilah yang terjadi pada mayoritas proses saat komputer dimatikan atau container dihentikan. Satu pengecualian mutlak perlu diingat:
Batas Event exit #
Satu aturan terakhir menyertai event exit pada contoh pertama: hanya kode sinkron yang dijamin berjalan di dalamnya. Operasi asynchronous seperti setTimeout atau fs.writeFile yang didaftarkan dari listener exit tidak akan pernah selesai — proses tidak menunggu event loop terisi kembali pada tahap itu, perilaku yang sudah kita buktikan pada chapter Module process di Section Core Modules. Karena itu susunlah pembersihan akhir secara sinkron, dan lakukan penyelamatan data yang berat di handler sinyal — sebelum memanggil process.exit() — bukan setelahnya.
Sinyal membuat proses kita bisa merespons dunia luar dengan sopan. Arah berikutnya justru sebaliknya: dari dalam Node.js, kita menjalankan proses lain, mengirim perintah, dan membaca hasilnya — kemampuan yang dibahas pada chapter Child Process.
Referensi lanjutan untuk topik chapter ini: