Penutup Bagian 2 meninggalkan satu eksperimen terbuka: tekan 5, ÷, 0, lalu =. Sebelum bagian ini, hasilnya bukan pesan yang bisa dipahami pengguna, melainkan Infinity — angka yang benar menurut aturan floating point, tetapi tidak bermakna di layar kalkulator. Kondisi serupa menyusup dari banyak arah: digit yang tidak pernah diketik, dua operasi yang ditekan berurutan, angka kedua yang lupa diisi. Bagian ketiga menutup semua celah itu dengan tiga lapis pertahanan: galat khusus bernama, guard clause di setiap titik masuk logika, dan satu tempat di antarmuka yang menampilkan pesan galat ke layar.

Galat Khusus untuk Kontrak yang Jelas #

Engine JavaScript sudah menyediakan galat bawaan seperti TypeError dan RangeError, tetapi galat-galat itu tidak membedakan “kesalahan pemrograman” dari “kondisi yang memang bisa terjadi saat kalkulator dipakai”. Pembagian dengan nol bukan bug program — pengguna memang boleh menekan tombolnya — sehingga memakai RangeError untuk kejadian normal akan membingungkan pembaca stack trace.

Solusinya adalah class galat sendiri, pola yang dibahas pada chapter Custom Errors:

js GalatKalkulator — src/logika.mjs
export class GalatKalkulator extends Error {
  constructor(pesan) {
    super(pesan);
    this.name = 'GalatKalkulator';
  }
}

Mari kita telaah apa yang terjadi pada baris-baris kode di atas:

  1. Baris 1 (extends Error): GalatKalkulator mewarisi seluruh perilaku Error — termasuk properti message dan stack — sehingga galat ini diperlakukan seperti galat lain oleh engine.
  2. Baris 3 (super(pesan)): pesan diteruskan ke constructor Error agar galat.message berisi teks yang kita tulis, bukan string kosong.
  3. Baris 4 (this.name): nama galat diganti dari generik "Error" menjadi "GalatKalkulator", sehingga transkrip galat langsung menyebut sumbernya.

Dengan class ini, pemisahan menjadi tegas: kode logika melempar GalatKalkulator untuk setiap kondisi yang bisa dipicu pengguna, sementara galat bawaan tetap berarti kesalahan pemrograman yang tidak boleh ditangkap dan disembunyikan.

Guard Clause di Setiap Titik Masuk #

Setiap function publik di logika.mjs kini memeriksa syaratnya lebih dulu dan langsung melempar galat bila syarat tidak terpenuhi — pola guard clause dari chapter Control Flow Best Practices. Berkas snapshot berikut adalah kondisi akhir logika.mjs:

js logika.mjs

Tiga guard clause yang paling menentukan:

  1. validasiDigit(digit) menolak karakter apa pun di luar 0–9. Tanpa pemeriksaan ini, satu panggilan inputDigit(state, 'x') cukup membuat state berisi teks yang tidak bisa dihitung.
  2. ubahKeAngka(teks) memvalidasi hasil konversi dengan Number.isNaN(). State angkaBaru berupa string, dan string apa pun bisa masuk lewat jalur yang tidak terduga; guard ini memastikan teks seperti "abc" tertangkap di gerbang, bukan di tengah perhitungan.
  3. hitung() menolak pembagian dengan nol sebelum operasi dijalankan — simbol === '/' && angkaKedua === 0. Tanpa guard ini, hasilnya Infinity, yang kemudian menular ke state berikutnya dan merusak seluruh rantai perhitungan.

Ada juga guard untuk urutan ketukan: pilihOperasi() menolak operasi kedua saat angkaBaru masih string kosong ('Dua operasi tidak boleh berurutan.'), dan samakan() menolak = saat angka kedua belum diketik ('Angka kedua belum dimasukkan.'). Keduanya menjaga state kalkulator agar tidak pernah berada di keadaan yang tidak terdefinisi.

Memancing Galat dari Terminal #

Karena semua guard berada di module logika yang bebas DOM, seluruh skenario galat bisa dibuktikan di terminal tanpa membuka browser. Berkas demo-galat.mjs memanggil setiap jalur galat di dalam try-catch:

Output yang Dihasilkan:

text
hitung(5, "/", 0) -> galat kalkulator: Tidak dapat membagi dengan nol.
inputDigit(state, "x") -> galat kalkulator: "x" bukan digit 0-9.
pilihOperasi(state, "*") -> galat kalkulator: Dua operasi tidak boleh berurutan.
samakan tanpa angka kedua -> galat kalkulator: Angka kedua belum dimasukkan.
pilihOperasi dengan "abc" -> galat kalkulator: "abc" bukan angka yang valid.
hitung(2, "+", 3) -> berhasil: 5

Mari kita telaah apa yang terjadi pada baris-baris kode di atas:

  1. Baris 8 (function coba): pembungkus kecil yang menjalankan satu tindakan dalam try-catch, mencetak hasil bila berhasil, dan mencetak pesan galat bila GalatKalkulator dilempar.
  2. Baris 24 (hitung(5, "/", 0)): guard pembagian nol terpicu, dan pesan yang tampil adalah teks yang kita tulis sendiri di logika.mjs — bukan Infinity.
  3. Baris 29 (hitung(2, "+", 3)): satu-satunya panggilan yang sah; keluarannya berhasil: 5, membuktikan bahwa penambahan guard tidak mengganggu jalur normal.

Perhatikan pola penting di dalam coba(): galat yang bukan GalatKalkulator dilempar kembali (throw galat). Menangkap galat bukan berarti menelan semua galat — keputusan penanganan tetap mengikuti kebijakan lapisan yang dibahas pada section Error Handling.

Menampilkan Galat di Layar #

Lapisan terakhir ada di antarmuka. Berkas src/index.html.txt membungkus setiap aksi tombol dengan try-catch: bila GalatKalkulator terlempar, isi message ditulis ke layar dengan textContent; bila galatnya bukan GalatKalkulator, galat tersebut dilempar kembali agar tetap terlihat di console developer. Pemilihan textContent — bukan innerHTML — mengikuti aturan Safe DOM Manipulation: pesan galat adalah data, bukan markup.

Enam uji baru ditambahkan di tests/logika.test.mjs, masing-masing mengunci satu skenario galat: digit di luar 0–9, dua operasi berurutan, teks bukan angka, pembagian nol, = tanpa angka kedua, dan skenario 5 / 0 dari ketukan tombol sampai galatnya muncul. Berikut hasil uji lengkap proyek pada kondisi akhir:

Output yang Dihasilkan:

text
▶ inputDigit
  ✔ digit pertama menggantikan nol awal (0.62933ms)
  ✔ digit berikutnya digabung menjadi angka lebih panjang (0.166489ms)
  ✔ karakter di luar digit 0-9 ditolak (0.376503ms)
✔ inputDigit (2.055712ms)
▶ pilihOperasi
  ✔ menyimpan angka pertama dan simbol operasi (0.271235ms)
  ✔ menyelesaikan rantai sebelumnya dari kiri ke kanan (0.151605ms)
  ✔ dua operasi berurutan ditolak (0.125516ms)
  ✔ teks yang bukan angka valid ditolak (0.122428ms)
✔ pilihOperasi (0.983489ms)
▶ hitung
  ✔ menjalankan empat operasi aritmetika (0.186665ms)
  ✔ simbol yang tidak dikenal ditolak (0.135173ms)
  ✔ pembagian dengan nol ditolak (0.226786ms)
✔ hitung (0.768984ms)
▶ samakan
  ✔ menampilkan hasil dan mengosongkan rantai (0.300863ms)
  ✔ tanpa operasi, state kembali tanpa perubahan (0.071774ms)
  ✔ pembulatan membersihkan noise desimal biner (0.060865ms)
  ✔ angka kedua yang belum dimasukkan ditolak (0.173409ms)
✔ samakan (0.764575ms)
▶ skenario mengetik
  ✔ alur 12 + 7 menampilkan 19 (0.197501ms)
  ✔ rantai 2 + 3 * 4 dievaluasi menjadi 20 (0.074474ms)
  ✔ layar menampilkan angka pertama saat operasi menunggu (0.042834ms)
  ✔ 5 / 0 sampai ke samakan sebagai GalatKalkulator (0.074049ms)
✔ skenario mengetik (0.530879ms)
ℹ tests 18
ℹ suites 5
ℹ pass 18
ℹ fail 0
ℹ skipped 0

Durasi di kolom kanan berbeda di setiap mesin; yang penting adalah angka pass 18, fail 0. Uji terakhir di kelompok skenario paling penting untuk dibaca: alur 5 → ÷ → 0 → = tidak lagi menghasilkan Infinity di state — samakan() melempar GalatKalkulator, dan state yang gagal tidak pernah disimpan.

Ringkasan Bagian 3 dan Penutup Proyek #

Kalkulator kita kini utuh dalam tiga lapis: logika murni yang menolak input tidak valid lewat guard clause, galat khusus GalatKalkulator yang memisahkan kondisi pengguna dari bug program, dan antarmuka yang menampilkan pesan galat ke layar tanpa menyembunyikan galat asing. Delapan belas uji menjaga setiap keputusan itu, sehingga perubahan berikutnya — menambah operasi persen, misalnya — cukup mengikuti pola yang sama: tambah aturan di OPERASI, tambah guard bila perlu, tambah uji.

Proyek ini juga meninggalkan satu kebiasaan yang layak dibawa ke semua proyek berikutnya: logika diuji di terminal, tampilan diuji di browser, dan keduanya tidak saling menembus. Keputusan desain itu yang membuat seluruh pengujian di proyek ini bisa berjalan dengan satu perintah node --test tanpa membuka satu pun halaman web.

Berkas Sumber

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

styles/styles.css

Unduh berkas
css
/* styles.css — tampilan kalkulator sederhana */

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #f3f4f6;
  font-family: system-ui, sans-serif;
}

.kalkulator {
  width: 240px;
  padding: 16px;
  background-color: #ffffff;
  border-radius: 12px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.layar {
  display: block;
  min-height: 48px;
  margin-bottom: 12px;
  padding: 10px 12px;
  background-color: #111827;
  color: #f9fafb;
  font-size: 2rem;
  text-align: right;
  white-space: nowrap;
  overflow-x: auto;
  border-radius: 8px;
}

.tombol-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
}

.tombol {
  padding: 12px 0;
  font-size: 1.25rem;
  border: none;
  border-radius: 8px;
  background-color: #e5e7eb;
  cursor: pointer;
}

.tombol:hover {
  background-color: #d1d5db;
}

.tombol--operasi {
  background-color: #fde68a;
}

.tombol--sama-dengan {
  background-color: #86efac;
}

.tombol--hapus {
  background-color: #fecaca;
}

.tombol--nol {
  grid-column: span 2;
}