Error yang lolos tanpa tertangkap membuat Node.js mencetak sekumpulan teks panjang ke terminal — bagi pemula, tumpukan itu sering terasa seperti sandi yang mustahil dibaca. Padahal teks tersebut menyimpan peta lengkap menuju sumber masalah: di berkas mana, di baris ke berapa, dan lewat jalur pemanggilan function seperti apa error itu terjadi. Keterampilan membaca stack trace adalah bekal debugging paling dasar sekaligus paling sering dipakai. Pada chapter ini kita akan membedah anatomi keluaran error, berlatih membacanya dari atas ke bawah, lalu mempelajari cause dan Error.captureStackTrace untuk menghasilkan jejak yang lebih jelas.
Anatomi Error yang Lolos #
Saat error tidak tertangkap try/catch, Node.js mencetak laporannya ke stderr lalu mengakhiri proses dengan exit code 1. Andaikan pemanggilan pada contoh bawah chapter ini dijalankan tanpa try/catch, terminal menampilkan laporan yang bentuknya seperti berikut — path di sini memakai contoh lokasi proyek fiktif:
file:///home/pengguna/toko/anatomi-stack.mjs:2
return item.harga;
^
TypeError: Cannot read properties of null (reading 'harga')
at ambilHarga (file:///home/pengguna/toko/anatomi-stack.mjs:2:15)
at hitungTotal (file:///home/pengguna/toko/anatomi-stack.mjs:6:10)
at prosesPesanan (file:///home/pengguna/toko/anatomi-stack.mjs:10:10)
at file:///home/pengguna/toko/anatomi-stack.mjs:14:15
at ModuleJob.run (node:internal/modules/esm/module_job:561:25)
at async node:internal/modules/esm/loader:647:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v24.21.0Setiap bagiannya punya peran:
| Bagian | Contoh | Artinya |
|---|---|---|
| Baris lokasi | file:///...anatomi-stack.mjs:2 | Berkas dan baris tempat error terjadi |
Cuplikan kode dan tanda ^ | return item.harga; | Isi baris tersebut; ^ menunjuk kolom yang dicurigai |
| Baris jenis error | TypeError: Cannot read properties... | Nama class error diikuti pesannya |
Bingkai at ... | at ambilHarga (...:2:15) | Jejak pemanggilan; format lokasinya berkas:baris:kolom |
| Baris versi | Node.js v24.21.0 | Versi runtime yang menjalankan kode |
Membaca Bingkai dari Atas ke Bawah #
Aturan terpenting dalam membaca stack trace: bingkai paling atas adalah tempat error terjadi, dan tiap bingkai di bawahnya adalah pemanggil function tersebut. Membaca ke bawah sama dengan menelusuri mundur jalur eksekusi. Contoh berikut menangkap error yang sama lalu mencetak ulang bingkainya dalam bentuk bernomor — path berkas dipangkas agar mudah dibaca:
function ambilHarga(item) {
return item.harga;
}
function hitungTotal(item, jumlah) {
return ambilHarga(item) * jumlah;
}
function prosesPesanan(item) {
return hitungTotal(item, 3);
}
try {
console.log(prosesPesanan(null));
} catch (err) {
console.log(`baris pertama : ${err.name}: ${err.message}`);
const bingkai = err.stack
.split("\n")
.slice(1)
.map((baris) => baris.trim())
.map((baris) => baris.replaceAll(import.meta.url, "anatomi-stack.mjs"));
bingkai.forEach((b, i) => {
const label = `bingkai ${i + 1}`.padEnd(13);
console.log(`${label} : ${b}`);
});
}Output yang Dihasilkan:
baris pertama : TypeError: Cannot read properties of null (reading 'harga')
bingkai 1 : at ambilHarga (anatomi-stack.mjs:2:15)
bingkai 2 : at hitungTotal (anatomi-stack.mjs:6:10)
bingkai 3 : at prosesPesanan (anatomi-stack.mjs:10:10)
bingkai 4 : at anatomi-stack.mjs:14:15
bingkai 5 : at ModuleJob.run (node:internal/modules/esm/module_job:561:25)
bingkai 6 : at async node:internal/modules/esm/loader:647:26
bingkai 7 : at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)Mari kita telaah bagian-bagian pentingnya:
- Baris 1-11 (rantai tiga function):
prosesPesananmemanggilhitungTotal, yang kemudian memanggilambilHarga— rantai inilah yang akan tercetak sebagai jejak. - Baris 14 (
prosesPesanan(null)): akar masalah sesungguhnya ada di sini — pemanggil memberi argumennull. Perhatikan bahwa error justru muncul jauh di dalamambilHarga. - Baris 16 (
err.namedanerr.message): propertinameberisi nama class error,messageberisi deskripsinya; keduanya membentuk baris pertamaerr.stack. - Baris 17-21 (pemrosesan
err.stack): stack dipecah per baris, baris pertama dibuang (slice(1)) karena sudah dicetak sendiri, sisanya dirapikan dan path absolut diganti nama berkas lewatreplaceAll(import.meta.url, ...)supaya laporan portabel. - Baris 22-25 (pencetakan bernomor): tiap bingkai diberi nomor urut agar urutan pembacaannya terlihat jelas.
Bacaan yang benar untuk keluaran di atas: error TypeError menyala di ambilHarga baris 2, dipanggil oleh hitungTotal baris 6, dipanggil oleh prosesPesanan baris 10, yang dipanggil dari level atas module pada baris 14. Tiga bingkai terakhir (ModuleJob.run dan kawan-kawan) berasal dari mekanisme pemuat module Node.js — bingkai node:internal seperti ini bukan bagian kode kita dan lazimnya dilewati saat membaca.
Posisi Sumber dalam Bingkai #
Lokasi pada tiap bingkai berformat berkas:baris:kolom. Baris dihitung dari atas berkas, sedangkan kolom dihitung dari karakter pertama baris tersebut. Bingkai at ambilHarga (anatomi-stack.mjs:2:15) menunjuk baris ke-2 kolom ke-15 — tepat di posisi harga pada return item.harga;. Kolom ini sama dengan posisi yang ditunjuk tanda ^ pada laporan mentah, sehingga kita bisa langsung melompat ke titik yang dicurigai tanpa membaca seluruh baris.
Function Bernama, Anonim, dan Frame Internal #
Bingkai dengan function bernama seperti at ambilHarga (...) paling mudah dilacak. Namun tidak semua bingkai bernama: bingkai at anatomi-stack.mjs:14:15 tanpa tanda kurung berasal dari kode level atas module yang tidak berada dalam function mana pun, sedangkan callback tanpa nama biasa tampil sebagai at Array.map (<anonymous>). Adanya bingkai anonim bukan berarti jejak rusak — hanya menandakan tidak ada nama yang bisa dilaporkan.
Bingkai node:internal/... juga sering muncul ketika error melibatkan mekanisme internal Node.js, misalnya pemuatan module atau pemrosesan promise. Bingkai tersebut membantu menjelaskan bagaimana kode kita sampai dijalankan, tetapi hampir selalu menjadi bising bagi pembacaan. Karena itu banyak logger dan perkakas memangkas bingkai internal sebelum menampilkan laporan.
Merangkai Penyebab dengan cause #
Sering kita perlu membungkus error tingkat rendah menjadi error yang lebih bermakna bagi pemanggil — misalnya SyntaxError dari berkas konfigurasi dibungkus menjadi “aplikasi tidak bisa mulai”. Tanpa penanganan khusus, pembungkusan justru membuang informasi aslinya. Opsi cause pada constructor Error menyimpan error lama di properti err.cause sehingga rantai penyebab tetap utuh:
function bacaKonfigurasi(namaFile) {
throw new Error(`gagal membaca ${namaFile}`, {
cause: new SyntaxError("token tidak terduga di baris 3"),
});
}
function jalankanAplikasi() {
try {
bacaKonfigurasi("konfigurasi.json");
} catch (err) {
throw new Error("aplikasi tidak bisa mulai", { cause: err });
}
}
try {
jalankanAplikasi();
} catch (err) {
console.log(`pesan : ${err.message}`);
console.log(`penyebab : ${err.cause.message}`);
console.log(`akar : ${err.cause.cause.name}: ${err.cause.cause.message}`);
}Output yang Dihasilkan:
pesan : aplikasi tidak bisa mulai
penyebab : gagal membaca konfigurasi.json
akar : SyntaxError: token tidak terduga di baris 3Tiga hal perlu dicermati dari contoh ini:
- Baris 2-4 (
new Error(pesan, { cause })): error baru menyimpan error asli sebagai penyebabnya; opsicausedidokumentasikan pada halaman errors Node.js . - Baris 9-11 (pembungkus kedua):
jalankanAplikasimenangkap error daribacaKonfigurasilalu membungkusnya sekali lagi — setiap lapisan menambah konteks tanpa menghapus lapisan sebelumnya. - Baris 16-19 (pembacaan rantai): properti
causebisa dirantai (err.cause.cause) hingga ke akar masalah; di sinilah letakSyntaxErroryang sebenarnya.
Menyembunyikan Bingkai Internal dengan captureStackTrace #
Pembuat library kerap menghadapi masalah sebaliknya: class error buatan sendiri membuat stack trace-nya tercemar bingkai constructor dan helper internal library, padahal pemakai hanya peduli dari titik mana mereka memanggil API itu. Error.captureStackTrace(target, fungsi) memotong semua bingkai sampai ke pemanggilan fungsi yang disebut:
class ErrorValidasi extends Error {
constructor(pesan) {
super(pesan);
this.name = "ErrorValidasi";
Error.captureStackTrace(this, ErrorValidasi);
}
}
function validasiEmail(email) {
if (!email.includes("@")) {
throw new ErrorValidasi(`email tidak valid: ${email}`);
}
return true;
}
function daftarPengguna(email) {
validasiEmail(email);
}
try {
daftarPengguna("budi.example.com");
} catch (err) {
console.log(`${err.name}: ${err.message}`);
const bingkai = err.stack
.split("\n")
.slice(1)
.map((b) => b.trim().replaceAll(import.meta.url, "tangkap-stack.mjs"));
console.log(`jumlah bingkai : ${bingkai.length}`);
bingkai.forEach((b, i) => {
const label = `bingkai ${i + 1}`.padEnd(14);
console.log(`${label}: ${b}`);
});
}Output yang Dihasilkan:
ErrorValidasi: email tidak valid: budi.example.com
jumlah bingkai : 6
bingkai 1 : at validasiEmail (tangkap-stack.mjs:11:11)
bingkai 2 : at daftarPengguna (tangkap-stack.mjs:17:3)
bingkai 3 : at tangkap-stack.mjs:21:3
bingkai 4 : at ModuleJob.run (node:internal/modules/esm/module_job:561:25)
bingkai 5 : at async node:internal/modules/esm/loader:647:26
bingkai 6 : at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)Perhatikan dua hal dari contoh ini:
- Baris 5 (
Error.captureStackTrace(this, ErrorValidasi)): bingkai constructor dannew ErrorValidasi(...)dipangkas, sehingga bingkai pertama langsung menunjukvalidasiEmail— titik pandang pemakai library. - Baris 24 (
jumlah bingkai : 6): bandingkan dengan contohanatomi-stack.mjsyang menghasilkan 7 bingkai; pemangkasan mengurangi bingkai tepat di posisi yang paling tidak informatif.
Membatasi Panjang Stack #
Pada error yang menjalar melalui banyak lapisan pemanggilan, stack trace bisa memanjang puluhan bingkai. Properti Error.stackTraceLimit membatasi jumlah bingkai yang dikumpulkan — berguna misalnya untuk log produksi yang cukup menampilkan beberapa bingkai teratas:
Error.stackTraceLimit = 2;
try {
null.harga;
} catch (err) {
const bingkai = err.stack
.split("\n")
.slice(1)
.map((b) => b.trim().replaceAll(import.meta.url, "cuplikan.mjs"));
console.log(bingkai.join("\n"));
}Output yang Dihasilkan:
at cuplikan.mjs:3:8
at ModuleJob.run (node:internal/modules/esm/module_job:561:25)Nilai bawaannya adalah 10; angka yang lebih kecil memangkas bingkai dari bawah, sedangkan 0 membuat stack hanya berisi baris pesannya. Detail cause sendiri juga tersedia di dokumentasi MDN
untuk rujukan lebih lanjut.
Membaca stack trace memberi kita peta kejadian setelah error terjadi. Namun peta semacam itu baru terlihat kalau error sempat tercetak — padahal banyak kejadian di aplikasi nyata bukan error, melainkan peristiwa biasa yang ingin kita amati: permintaan masuk, keputusan bisnis, hasil operasi. Untuk itu kita perlu teknik logging yang lebih sadar tujuan, topik Chapter Teknik Logging.