Ketika toko online menyimpan pesanan, kegagalannya beragam: kode kupon tidak dikenal, saldo tidak cukup, alamat belum lengkap. Dengan Error generik, semuanya tercatat sebagai Error dengan message berbeda — dan kode penanganan hanya bisa menebak dari isi teks. Cara yang lebih terstruktur adalah membuat jenis galat milik aplikasi sendiri, sehingga penanganan bisa memutuskan berdasarkan jenisnya, bukan berdasarkan hasil baca teks.

Alatnya adalah custom error: sebuah class yang menurunkan Error. Hasilnya terlihat langsung di transkrip galat — alih-alih Error: ..., Node menampilkan KuponTidakValid: ... — dan lebih penting lagi, penanganan dapat memeriksa jenisnya lewat instanceof, bukan menebak dari isi teks. Satu catatan dulu: class sebagai konsep dibahas penuh di section Object-Oriented JavaScript. Di chapter ini kita hanya memakai sintaks minimum yang diperlukan — semuanya sudah diuji dan cukup untuk keperluan galat — tanpa perlu memahami seluruh teori class terlebih dahulu.

Struktur Minimum sebuah Custom Error #

Pola minimalnya terdiri dari tiga bagian: menurunkan Error, meneruskan pesan ke Error, dan mengganti name:

js
class KuponTidakValid extends Error {
  constructor(message) {
    super(message);
    this.name = "KuponTidakValid";
  }
}

const galat = new KuponTidakValid("kode tidak dikenal");

console.log(galat.name);
console.log(galat.message);
console.log(galat instanceof Error);

Output yang Dihasilkan:

text
KuponTidakValid
kode tidak dikenal
true

Mari kita bedah tiga baris kuncinya:

  1. Baris 1 (class KuponTidakValid extends Error {): kata kunci extends menurunkan Error — KuponTidakValid mewarisi seluruh perilaku dasarnya, termasuk properti stack.
  2. Baris 3 (super(message);): meneruskan pesan ke constructor Error. Tanpa baris ini, message tidak pernah terisi, dan stack tidak direkam dengan benar. Baris ini wajib menjadi pernyataan pertama di constructor.
  3. Baris 4 (this.name = "KuponTidakValid";): mengganti name bawaan. Menariknya, engine tidak mengisi name otomatis dari nama class — tanpa baris ini, galat.name tetap tercetak "Error", label generik yang justru ingin kita hindari. Penugasan satu baris ini wajib diulang di setiap class galat.

Hasilnya: galat.name berisi "KuponTidakValid", message tersimpan seperti biasa, dan instanceof Error tetap true — galat kustom ini masih anggota keluarga Error, jadi semua pola try-catch yang sudah kita pelajari tetap berlaku.

Membedakan Jenis Kegagalan dengan instanceof #

Sekarang bagian yang memberi manfaat sesungguhnya: operator instanceof memungkinkan catch memutuskan berdasarkan jenis galat. Dengan Error generik, satu-satunya pembeda adalah isi message — dan menganalisis teks di dalam program adalah pekerjaan yang rapuh. Dengan custom error, pembedanya adalah jenis object-nya sendiri: pasti, tidak bergantung kata pilihan, dan tidak berubah saat pesan dirapikan. Berkas berikut mendefinisikan dua jenis kegagalan toko, lalu menanganinya dengan cara berbeda:

js kupon-toko.js
// kupon-toko.js
// Custom Error membuat penanganan bisa membedakan jenis kegagalan.

class KuponTidakValid extends Error {
  constructor(message) {
    super(message);
    this.name = "KuponTidakValid";
  }
}

class SaldoTidakCukup extends Error {
  constructor(message) {
    super(message);
    this.name = "SaldoTidakCukup";
  }
}

function terapkanKupon(kode, daftarKupon) {
  if (!daftarKupon.includes(kode)) {
    throw new KuponTidakValid("Kode kupon tidak dikenal: " + kode);
  }
  return "Diskon diterapkan untuk kupon " + kode;
}

function bayar(total, saldo) {
  if (saldo < total) {
    throw new SaldoTidakCukup("Saldo kurang Rp" + (total - saldo));
  }
  return "Pembayaran berhasil";
}

const percobaan = [
  () => terapkanKupon("HEMAT10", ["HEMAT10", "GRATISONGKIR"]),
  () => terapkanKupon("SALAHKODE", ["HEMAT10", "GRATISONGKIR"]),
  () => bayar(100000, 75000),
];

for (const aksi of percobaan) {
  try {
    console.log(aksi());
  } catch (error) {
    if (error instanceof KuponTidakValid) {
      console.log("[kupon] " + error.message);
    } else if (error instanceof SaldoTidakCukup) {
      console.log("[saldo] " + error.message + " - silakan top up");
    } else {
      throw error; // galat yang tidak dikenal diteruskan ke atas
    }
  }
}

Output yang Dihasilkan:

text
Diskon diterapkan untuk kupon HEMAT10
[kupon] Kode kupon tidak dikenal: SALAHKODE
[saldo] Saldo kurang Rp25000 - silakan top up

Mari kita telaah bagian-bagian pentingnya:

  1. Baris 4–9 dan 11–16 (dua definisi class): tiap jenis kegagalan mendapat class-nya sendiri dengan name yang bermakna. Dua class ini identik strukturnya — pembedanya hanya nama.
  2. Baris 18–23 (function terapkanKupon(...)): fungsi memeriksa kupon dengan includes dari section Arrays. Bila kode tidak terdaftar, galat jenis KuponTidakValid dilempar — bukan Error generik.
  3. Baris 25–30 (function bayar(...)): pola yang sama untuk saldo. bayar(100000, 75000) melempar SaldoTidakCukup dengan pesan yang menghitung kekurangannya, Rp25000.
  4. Baris 42–45 (if (error instanceof KuponTidakValid) { ... }): catch kini bisa bercabang berdasarkan jenis. Kupon tidak dikenal dicatat dengan label [kupon]; saldo kurang dengan label [saldo] dan saran top up. Dua kegagalan, dua perlakuan, satu blok try-catch.
  5. Baris 46–48 (else { throw error; }): cabang terakhir meneruskan galat yang tidak dikenal ke atas. Prinsip ini penting: penanganan khusus hanya untuk jenis yang kita kenal; sisanya tidak ditelan.

Penanganan berbasis jenis ini juga mudah diperluas. Saat toko menambah kegagalan baru — alamat belum lengkap, misalnya — pekerjaannya terukur: satu class galat, satu cabang instanceof, dan sisa program tidak tersentuh. Bandingkan bila semua kegagalan hanya dibedakan lewat message: setiap penambahan jenis berarti menambah pencocokan teks yang mudah salah tempat.

Menambah Detail Kegagalan #

message hanya satu string. Sering kali penanganan butuh detail terstruktur — kode kupon yang gagal, misalnya — tanpa harus membaca ulang teks pesan. Karena class milik kita sendiri, constructor boleh menerima parameter tambahan dan menyimpannya sebagai properti:

js
class KuponTidakValid extends Error {
  constructor(message, kodeKupon) {
    super(message);
    this.name = "KuponTidakValid";
    this.kodeKupon = kodeKupon;
  }
}

try {
  throw new KuponTidakValid("Kode kupon tidak dikenal", "SALAHKODE");
} catch (error) {
  console.log(error.name);
  console.log(error.kodeKupon);
}

Output yang Dihasilkan:

text
KuponTidakValid
SALAHKODE

Aturannya konsisten dengan struktur minimum: super(message) tetap menjadi pernyataan pertama, dan properti tambahan baru dilekatkan setelahnya. catch kini bisa membaca error.kodeKupon secara langsung — data kegagalan yang presisi, tanpa mengandalkan isi teks pesan.

Batasannya juga jelas: properti tambahan hanya dikenal oleh kode aplikasi kita. Engine, transkrip Node, maupun alat lain tidak memberinya perlakuan khusus — name, message, dan stack tetap satu-satunya informasi yang dijamin tersedia di semua alat. Karenanya properti tambahan cocok untuk detail pelaporan, bukan pengganti ketiganya.

Stack Tetap Terekam #

Satu kekhawatiran yang wajar muncul: apakah galat buatan sendiri masih membawa stack? Jawabannya ya, dan ini bisa dibuktikan dengan class dan function dari berkas contoh di atas:

js
try {
  terapkanKupon("SALAHKODE", ["HEMAT10"]);
} catch (error) {
  console.log(error instanceof KuponTidakValid);
  console.log(error instanceof Error);
  console.log(error.stack.split("\n")[0]);
}

Output yang Dihasilkan:

text
true
true
KuponTidakValid: Kode kupon tidak dikenal: SALAHKODE

Baris pertama dari stack memakai name kustom kita — KuponTidakValid: ... — lalu menyusul lokasi berkas dan baris, persis seperti galat bawaan. Sejak ES2022, perilaku stack diatur oleh spesifikasi, dan class yang menurunkan Error secara native seperti di atas merekamnya dengan benar. Keduanya — instanceof yang true sampai ke Error dan stack yang utuh — membuat custom error aman dipakai di seluruh lapisan program.

Kapan Layak Membuat Custom Error #

Custom error adalah penambahan kode, jadi pemakaiannya perlu dibenarkan. Tiga kondisi berikut biasanya menjadi alasan yang cukup:

  • Penanganan perlu berbeda antar jenis kegagalan. Bila semua kegagalan diperlakukan sama, Error generik dengan message sudah cukup.
  • Istilah domain lebih bermakna. SaldoTidakCukup menyatakan keadaan bisnis secara langsung; padanannya di TypeError generik tidak menyentuh makna itu.
  • Jenis kegagalan tumbuh mengikuti fitur. Setiap fitur baru membawa kegagalan barunya; nama jenis yang eksplisit menjaga rantai instanceof tetap terbaca.

Pada project kecil dengan satu jenis kegagalan, menahan diri dari membuat class juga keputusan yang benar. Mulailah dari Error generik, lalu naikkan ke custom error ketika keputusan penanganan mulai bercabang — kriteria paling jujur adalah munculnya kebutuhan instanceof di blok catch.

Ketika aplikasi tumbuh menjadi banyak lapisan, muncul pertanyaan baru: jenis galat seperti apa yang ditangani di lapisan mana, dan galat yang tidak tertangani akhirnya berhenti di mana? Pertanyaan desain itulah topik chapter berikutnya.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

kupon-toko.js

Unduh berkas
javascript
// kupon-toko.js
// Custom Error membuat penanganan bisa membedakan jenis kegagalan.

class KuponTidakValid extends Error {
  constructor(message) {
    super(message);
    this.name = "KuponTidakValid";
  }
}

class SaldoTidakCukup extends Error {
  constructor(message) {
    super(message);
    this.name = "SaldoTidakCukup";
  }
}

function terapkanKupon(kode, daftarKupon) {
  if (!daftarKupon.includes(kode)) {
    throw new KuponTidakValid("Kode kupon tidak dikenal: " + kode);
  }
  return "Diskon diterapkan untuk kupon " + kode;
}

function bayar(total, saldo) {
  if (saldo < total) {
    throw new SaldoTidakCukup("Saldo kurang Rp" + (total - saldo));
  }
  return "Pembayaran berhasil";
}

const percobaan = [
  () => terapkanKupon("HEMAT10", ["HEMAT10", "GRATISONGKIR"]),
  () => terapkanKupon("SALAHKODE", ["HEMAT10", "GRATISONGKIR"]),
  () => bayar(100000, 75000),
];

for (const aksi of percobaan) {
  try {
    console.log(aksi());
  } catch (error) {
    if (error instanceof KuponTidakValid) {
      console.log("[kupon] " + error.message);
    } else if (error instanceof SaldoTidakCukup) {
      console.log("[saldo] " + error.message + " - silakan top up");
    } else {
      throw error; // galat yang tidak dikenal diteruskan ke atas
    }
  }
}