Sampai di sini kita menguasai peralatannya satu per satu: throw untuk melempar, try-catch-finally untuk menangkap, dan custom error untuk menamai jenis kegagalan. Pertanyaan yang tersisa bukan lagi tentang sintaks, melainkan tentang rancangan: pada aplikasi yang tersusun dari banyak lapisan — function kecil, modul, entry point — galat boleh muncul di mana saja. Maka keputusan pentingnya adalah: di lapisan mana galat ditangani, dan kapan galat diteruskan?

Anggap program kita seperti operasional toko. Kasir menyelesaikan transaksi harian sendiri; kasus yang di luar kewenangannya — misalnya harga yang tidak terdaftar — diteruskan ke supervisor; hanya persoalan yang tak tertangani di tingkat mana pun yang akhirnya sampai ke pemilik toko. Tidak ada satu orang pun yang harus tahu cara menyelesaikan semuanya. Prinsip yang sama berlaku pada program: tiap lapisan menangani yang bisa, dan membiarkan sisanya naik. Rancangan tentang siapa menangani apa inilah yang disebut batas galat (error boundary).

Prinsip: Tangani Bila Tahu, Teruskan Bila Tidak #

Aturannya: sebuah lapisan menangkap galat hanya bila lapisan itu tahu cara memulihkannya. Bila tidak, blok catch boleh dikosongkan dari penanganan dan meneruskan galat dengan throw error — kebiasaan yang disebut rethrow. Galat yang diteruskan akan dicari oleh catch terdekat di lapisan di atasnya.

Sebelum menulis blok catch, tiga pertanyaan membantu memutuskan penempatannya:

  • Apa jenis galatnya? Input salah dapat dipulihkan (minta ulang, isi ulang); kesalahan programmer seperti salah ketik nama variabel sebaiknya dibiarkan terlihat.
  • Apakah lapisan ini punya solusinya? Lapisan bawah biasanya tidak tahu kebijakan aplikasi; ia hanya tahu datanya salah.
  • Apa yang terjadi bila galat diteruskan? Galat akan dicari penangkapnya di atas; bila sampai di Node tanpa penangkap, program berhenti — dan itu memang tempatnya untuk galat yang tidak siap ditangani.

Mekanisme naiknya galat:

  • throw mencari blok catch terdekat pada rantai pemanggil, dari dalam ke luar.
  • catch yang menangkap bebas memilih: memulihkan dan melanjutkan, atau meneruskan dengan throw error.
  • Bila sampai di titik masuk program pun tak ada penangkap, Node menghentikan program dan mencetak transkrip galat.

Berkas snippet berikut menunjukkan satu lapisan yang menangani satu jenis dan meneruskan jenis lain:

js
function perbaruiStok(jumlah) {
  try {
    if (typeof jumlah !== "number") {
      throw new TypeError("jumlah harus berupa number");
    }
    if (jumlah < 0) {
      throw new RangeError("jumlah tidak boleh negatif");
    }
    console.log("Stok diperbarui:", jumlah);
  } catch (error) {
    if (error instanceof TypeError) {
      console.log("Dipulihkan: input dianggap nol");
      return;
    }
    throw error; // galat lain bukan urusan lapisan ini
  }
}

perbaruiStok(12);
perbaruiStok("dua belas");

try {
  perbaruiStok(-5); // RangeError: diteruskan, bukan ditangani di sini
} catch (error) {
  console.log("Sampai di batas atas:", error.name);
}

Output yang Dihasilkan:

text
Stok diperbarui: 12
Dipulihkan: input dianggap nol
Sampai di batas atas: RangeError

Mari kita baca keputusan yang diambil:

  1. Baris 4–6 (if (typeof jumlah !== "number") { ... }): tipe yang salah dapat dipulihkan di sini — lapisan tahu cara mengatasinya, jadi galat ditangani dan loop kerja berlanjut.
  2. Baris 7–9 (if (jumlah < 0) { ... }): stok negatif adalah masalah data yang tidak boleh ditutupi dengan angka buatan. Lapisan ini sengaja tidak menanganinya.
  3. Baris 12–15 (if (error instanceof TypeError) { ... return; }): blok catch menyaring dengan instanceof dari chapter Custom Errors. Hanya TypeError yang dipulihkan.
  4. Baris 16 (throw error;): RangeError dilempar ulang tanpa diubah. Pemanggil menerima galat yang sama persis — lengkap dengan name, message, dan stack-nya — dan lapisan atas yang memutuskan nasibnya.

Satu Batas Terpusat di Entry Point #

Ketika setiap lapisan meneruskan galat yang tidak dikuasainya, galat-galat itu akhirnya berkumpul di satu tempat: titik masuk program. Di sanalah satu batas terpusat (centralized error boundary) diletakkan — satu-satunya tempat yang menangkap segala galat, mencatatnya secara seragam, dan menjaga program tetap berjalan untuk pekerjaan berikutnya.

Berkas berikut menyusun semuanya: lapisan bawah melempar, lapisan tengah meneruskan, entry point menangkap:

js kasir-toko.js
// kasir-toko.js
// Batas galat: lapisan bawah melempar, entry point menangkap semuanya.

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

function validasiJumlah(jumlah) {
  if (!Number.isInteger(jumlah) || jumlah < 1) {
    throw new GalatValidasi("Jumlah harus bilangan bulat minimal 1");
  }
}

function prosesPesanan(nama, jumlah) {
  validasiJumlah(jumlah); // galat dibiarkan naik ke pemanggil
  return "Pesanan " + nama + " x" + jumlah + " diproses";
}

const pesananHariIni = [
  ["Buku Catatan", 3],
  ["Pulpen", 0],
  ["Tas Sekolah", 2],
];

for (const [nama, jumlah] of pesananHariIni) {
  try {
    console.log(prosesPesanan(nama, jumlah));
  } catch (error) {
    console.log("[CATAT KEGAGALAN] " + error.name + ": " + error.message);
  }
}

console.log("Kasir siap menerima pesanan berikutnya");

Output yang Dihasilkan:

text
Pesanan Buku Catatan x3 diproses
[CATAT KEGAGALAN] GalatValidasi: Jumlah harus bilangan bulat minimal 1
Pesanan Tas Sekolah x2 diproses
Kasir siap menerima pesanan berikutnya

Mari kita telusuri peran tiap lapisan:

  1. Baris 11–15 (function validasiJumlah(jumlah) { ... }): lapisan paling bawah. Tugasnya satu: menolak data yang tidak valid dengan melempar GalatValidasi. Ia tidak tahu — dan tidak perlu tahu — bagaimana kegagalan itu dilaporkan.
  2. Baris 17–20 (function prosesPesanan(nama, jumlah) { ... }): lapisan tengah. Ia memanggil validasiJumlah tanpa try-catch apa pun; galatnya dibiarkan naik ke pemanggil. Fokusnya murni pada proses pesanan yang sah.
  3. Baris 28–34 (for (const [nama, jumlah] of pesananHariIni) { try { ... } catch (error) { ... } }): batas terpusat. Seluruh pesanan hari ini melewati satu blok try-catch yang sama, dan format pelaporannya hanya ditulis sekali: [CATAT KEGAGALAN].
  4. Baris 36 (console.log("Kasir siap menerima pesanan berikutnya");): pesanan ["Pulpen", 0] gagal, tetapi pesanan berikutnya tetap diproses dan baris ini tercapai. Satu kegagalan lokal tidak menjatuhkan keseluruhan program.

Manfaat desain ini terasa saat program berkembang. Perubahan format pencatatan — misalnya menambah stempel waktu — cukup dilakukan di satu tempat. Lapisan bawah tetap bersih dari kode pelaporan, dan tiap fungsi hanya mengurus satu tanggung jawab.

Batas Paling Luar di Node #

Batas terpusat pada berkas contoh masih ditulis manual di entry point. Node juga menyediakan jaring paling luar untuk galat yang lolos dari semua lapisan: event uncaughtException pada object process. Handler-nya dipanggil sebelum Node menutup program, sehingga kegagalan terakhir masih sempat dicatat:

js
process.on("uncaughtException", (error) => {
  console.log("Batas terakhir menangkap:", error.name, "-", error.message);
});

throw new Error("lolos dari semua lapisan");

console.log("Baris ini tidak tercapai");

Output yang Dihasilkan:

text
Batas terakhir menangkap: Error - lolos dari semua lapisan

Dua perilakunya perlu dicermati. Program tidak berhenti mendadak sebelum handler berjalan — keluaran handler tercetak utuh. Namun baris setelah throw tetap tidak dilanjutkan: eksekusi di titik kegagalan sudah berhenti, handler hanya menyisakan kesempatan mencatat, bukan melanjutkan pekerjaan. Karena itu uncaughtException cocok untuk pencatatan terakhir dan pembersihan, bukan untuk alur kerja normal — batas terpusat berupa try-catch tetap menjadi fondasi rancangan.

Di sisi browser, pola yang sama tersedia lewat handler galat global pada window — topik yang akan terasa relevan begitu kita masuk ke section DOM.

Peta Alur sebuah Galat #

Diagram berikut merangkum perjalanan satu galat dari lapisan bawah sampai batas terpusat:

Dari atas ke bawah, keputusannya selalu sama: lapisan mana pun boleh menangani bila mampu; bila tidak, galat naik tanpa diubah; dan entry point menjamin kegagalan terakhir dicatat tanpa menjatuhkan program. Karena setiap jalur berakhir di batas terpusat, tidak ada galat yang berhenti tanpa tercatat — syarat minimal agar masalah bisa diperbaiki pada hari yang sama.

Sistem ini hanya selengkap catatan yang ditulisnya. Ketika kegagalan memang terjadi, pekerjaan berikutnya adalah membaca jejak yang ditinggalkan galat itu — transkrip, stack trace, dan debugger — dan itulah materi chapter terakhir section ini.

Berkas Sumber

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

kasir-toko.js

Unduh berkas
javascript
// kasir-toko.js
// Batas galat: lapisan bawah melempar, entry point menangkap semuanya.

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

function validasiJumlah(jumlah) {
  if (!Number.isInteger(jumlah) || jumlah < 1) {
    throw new GalatValidasi("Jumlah harus bilangan bulat minimal 1");
  }
}

function prosesPesanan(nama, jumlah) {
  validasiJumlah(jumlah); // galat dibiarkan naik ke pemanggil
  return "Pesanan " + nama + " x" + jumlah + " diproses";
}

const pesananHariIni = [
  ["Buku Catatan", 3],
  ["Pulpen", 0],
  ["Tas Sekolah", 2],
];

for (const [nama, jumlah] of pesananHariIni) {
  try {
    console.log(prosesPesanan(nama, jumlah));
  } catch (error) {
    console.log("[CATAT KEGAGALAN] " + error.name + ": " + error.message);
  }
}

console.log("Kasir siap menerima pesanan berikutnya");