Program yang tidak pernah salah hanyalah program yang tidak pernah berjalan: berkas konfigurasi bisa tidak ada, masukan pengguna bisa tidak lengkap, koneksi database bisa terputus. Bahasa pemrograman yang matang karena itu menyediakan mekanisme untuk melontarkan dan menangkap kegagalan secara terstruktur, dan mekanisme itu di JavaScript adalah throw bersama try-catch-finally. Kita sudah beberapa kali memakainya di chapter-chapter sebelumnya untuk menangkap TypeError dan error validasi; sekarang saatnya membedahnya secara menyeluruh. Pada chapter ini kita akan membongkar anatomi object Error, memetakan alur eksekusi try-catch-finally, lalu membangun class error kustom agar kegagalan bisa ditangani selektif.
Anatomi Object Error #
Error di JavaScript adalah object, bukan sekadar string — dan itu membuat perbedaan besar. Karena berupa object, sebuah error membawa nama, pesan, dan metadata tambahan yang bisa kita baca secara terprogram, termasuk properti cause untuk merekam error asli yang memicu error baru. Berkas error-dasar.mjs membedah isi sebuah object Error:
const errorBiasa = new Error("Koneksi ke database gagal");
console.log(`name : ${errorBiasa.name}`);
console.log(`message : ${errorBiasa.message}`);
// cause: merekam error asli yang memicu error baru
const errorAsli = new Error("ECONNREFUSED: koneksi ditolak server");
const errorBaru = new Error("Gagal memuat konfigurasi", { cause: errorAsli });
console.log(`cause.name : ${errorBaru.cause.name}`);
console.log(`cause.message : ${errorBaru.cause.message}`);Output yang Dihasilkan:
name : Error
message : Koneksi ke database gagal
cause.name : Error
cause.message : ECONNREFUSED: koneksi ditolak serverDua property pertama adalah wajah utama setiap error. Property name berisi nama class error ("Error" untuk error generik, "TypeError", "ReferenceError", dan seterusnya untuk varian bawaan), sedangkan message berisi deskripsi kegagalannya. Opsi { cause: errorAsli } pada constructor menjawab kebutuhan klasik: ketika satu kegagalan level rendah (koneksi ditolak) memicu kegagalan level tinggi (konfigurasi tidak termuat), error baru tetap menyimpan jejak penyebabnya tanpa harus menumpuk keduanya dalam satu pesan.
throw, try, catch, dan finally #
Kata kunci throw melempar sebuah nilai — lazimnya object Error — dan menghentikan eksekusi normal saat itu juga: baris-baris setelah throw tidak pernah berjalan. Eksekusi melompat ke blok catch terdekat yang sanggup menangkapnya, dan blok finally selalu berjalan baik setelah try sukses maupun setelah catch menangani kegagalan. Mari kita lihat keduanya dalam aksi dengan snippet pendek:
function ambilNilai(data, kunci) {
if (!(kunci in data)) {
throw new Error(`kunci ${kunci} tidak ditemukan`);
}
return data[kunci];
}
try {
console.log(ambilNilai({ versi: 1 }, "versi"));
console.log(ambilNilai({ versi: 1 }, "port"));
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}Output yang Dihasilkan:
1
Error: kunci port tidak ditemukanPemanggilan pertama berhasil mencetak 1. Pemanggilan kedua menemukan kunci "port" yang tidak ada, sehingga throw dieksekusi — perhatikan bahwa console.log terluar tidak pernah mencetak nilai kembalian kedua, karena eksekusi langsung melompat ke catch. Alur lengkap try-catch-finally dengan cabang sukses dan gagal dirangkum dalam try-catch-finally.mjs:
function bacaKonfigurasi(berkas) {
if (!berkas) {
throw new Error("nama berkas konfigurasi kosong");
}
return `konfigurasi dari ${berkas}`;
}
function muatAplikasi(berkas) {
console.log("mulai : memuat aplikasi");
try {
const hasil = bacaKonfigurasi(berkas);
console.log(`sukses : ${hasil}`);
return "aplikasi siap";
} catch (e) {
console.log(`tangkap : ${e.name}: ${e.message}`);
return "aplikasi memakai konfigurasi bawaan";
} finally {
console.log("finally : menutup semua resource");
}
}
console.log(`hasil : ${muatAplikasi("app.json")}`);
console.log("---");
console.log(`hasil : ${muatAplikasi("")}`);Output yang Dihasilkan:
mulai : memuat aplikasi
sukses : konfigurasi dari app.json
finally : menutup semua resource
hasil : aplikasi siap
---
mulai : memuat aplikasi
tangkap : Error: nama berkas konfigurasi kosong
finally : menutup semua resource
hasil : aplikasi memakai konfigurasi bawaanBacalah kedua sesi pada output sebagai dua skenario:
- Skenario sukses (
app.jsonterisi):tryberjalan sampaireturn,catchdilewati,finallytetap dieksekusi sebelum nilai benar-benar dikembalikan ke pemanggil. - Skenario gagal (argumen kosong):
throwdi dalambacaKonfigurasimelemparErrorkeluar dari function,catchmenangkapnya sebagai parametere, lalu function mengembalikan nilai cadangan.finallykembali dieksekusi — tepat di sinilah letak kegunaannya: kode pembersihan seperti menutup berkas atau koneksi tidak perlu diduplikasi di dua cabang.
Membuat Class Error Kustom #
Saat aplikasi tumbuh, satu jenis Error generik tidak lagi cukup: error validasi masukan harus ditangani berbeda dari error jaringan. Karena Error adalah class biasa, kita tinggal menurunkannya — materi extends dari Chapter Class dan Prototype langsung terpakai. Berkas error-kustom.mjs membangun ValidationError yang membawa nama field bermasalah:
class ValidationError extends Error {
constructor(field, pesan) {
super(pesan);
this.name = "ValidationError";
this.field = field;
}
}
function validasiPendaftaran(data) {
if (!data.email) {
throw new ValidationError("email", "email wajib diisi");
}
if (!data.email.includes("@")) {
throw new ValidationError("email", "format email tidak sah");
}
if (data.umur < 17) {
throw new ValidationError("umur", "umur minimal 17 tahun");
}
return "data pendaftaran valid";
}
function proses(data) {
try {
return validasiPendaftaran(data);
} catch (e) {
if (e instanceof ValidationError) {
return `perbaiki field ${e.field}: ${e.message}`;
}
throw e; // error yang tidak dikenal diteruskan ke pemanggil atas
}
}
console.log(proses({ email: "[email protected]", umur: 20 }));
console.log(proses({ email: "budi", umur: 20 }));
console.log(proses({ email: "[email protected]", umur: 15 }));Output yang Dihasilkan:
data pendaftaran valid
perbaiki field email: format email tidak sah
perbaiki field umur: umur minimal 17 tahunTiga keputusan desain pada kode ini layak dicermati:
super(pesan)di constructor meneruskan pesan ke constructorErroragare.messageterisi normal;this.name = "ValidationError"kemudian mengganti label agar output log langsung terbaca jelas.- Property tambahan
this.fieldadalah nilai tambah class kustom: handler tidak perlu mengurai pesan string untuk tahu field mana yang salah — cukup bacae.field. if (e instanceof ValidationError)dicatchmenangani hanya error yang dikenalnya; error lain dilempar kembali lewatthrow e. Pola selektif ini menjaga bug tak terduga tetap terlihat, bukan ditelan diam-diam.
Pola Penanganan yang Sehat #
Empat kebiasaan berikut merangkum praktik penanganan error di kode Node.js yang matang. Pertama, lempar Error sungguhan (throw new Error(...)), bukan string — string tidak membawa name, stack, dan tidak lolos instanceof. Kedua, tangani error pada lapisan yang tahu apa yang harus dilakukan: function tingkat rendah melempar, lapisan aplikasi memutuskan antara mencoba ulang, memakai nilai cadangan, atau menggagalkan operasi. Ketiga, tambahkan konteks saat melempar ulang dengan opsi cause agar rantai penyebab tidak putus. Keempat, biarkan finally yang menangani pembersihan resource agar tidak ada jalur eksekusi yang lupa menutup berkas atau koneksi. Kita akan melihat keempatnya bekerja dalam proyek-proyek di bagian akhir Course ini.
Referensi lanjutan untuk topik chapter ini:
Kita kini bisa menjaga program tetap berdiri ketika kegagalan synchronous terjadi. Namun operasi andalan Node.js — membaca berkas, memanggil API, query database — bersifat asynchronous: hasilnya baru tiba setelah fungsi pemanggil selesai. Kegagalan di dunia itu tidak bisa ditangkap try-catch biasa; mereka bepergian lewat promise. Chapter Promise dan async-await membahas tuntas topik tersebut.