Error handling adalah keputusan tentang apa yang terjadi saat sesuatu salah. Tujuannya satu: masalahnya terdengar, jelas, dan sampai ke orang yang bisa memperbaikinya. Bayangin pembeli bilang ke kasir, "Pakai voucher HEMAT01 ya." Kasir yang baik menjawab, "Kode itu gak ada, mungkin maksudnya HEMAT10?" Kasir yang buruk diam saja dan menagih harga penuh. Pembeli pulang mengira dapat potongan, dan gak ada yang tahu ada yang salah.
🧪 Tebakan cepat:
applyVouchermengembalikannulluntuk voucher yang gak dikenal, dancalculateTotallupa mengeceknya. Keranjang A (Rp 245.000) denganHEMAT01: hasilnya error,Total: Rp 245.000, atau yang lain? Tebak dulu, jawabannya ada di bagian "return null: masalahnya pindah tempat".
Dua hal yang bisa salah di kasir
Kasir Toko Kaos Raka dari lesson Fungsi Kecil & Fokus gak pernah mengeluh. applyVoucher mengabaikan semua kode selain HEMAT10, dan calculateTotal menerima keranjang apa saja:
> calculateTotal(cartA, 'HEMAT01')
'Total: Rp 245.000'
> calculateTotal([], '')
'Total: Rp 15.000'
Voucher salah ketik diabaikan diam-diam, jadi pembeli mengira potongannya masuk. Keranjang kosong ditagih Rp 15.000: subtotal 0 di bawah 200.000, jadi kena ongkir untuk nol barang. Dua-duanya gak error, dua-duanya salah. Ada tiga cara melapornya.
return null: masalahnya pindah tempat
Cara pertama: kembalikan null, "gak ada hasil".
function applyVoucher(amount, voucherCode) {
if (voucherCode === '') return amount;
if (voucherCode === 'HEMAT10') return amount - amount * VOUCHER_DISCOUNT;
return null; // voucher gak dikenal
}
calculateTotal gak mengecek null dan meneruskannya ke addShipping. null < 200000 hasilnya true, dan null + 15000 hasilnya 15000, karena JavaScript menghitung null sebagai 0:
> calculateTotal(cartA, 'HEMAT01')
'Total: Rp 15.000'
Itu jawaban tebakannya: bukan error, tapi tagihan Rp 15.000 untuk belanja Rp 245.000. null cuma memindahkan masalahnya ke pemanggil, dan pemanggil yang lupa mengecek gak diingatkan apa-apa.
throw: berhenti dengan keras
Cara kedua: lempar error. Pemanggil gak bisa pura-pura gak lihat; kalau gak ditangkap, programnya berhenti dan console menunjuk masalahnya.
class VoucherError extends Error {
constructor(code) {
super('Voucher ' + code + ' tidak dikenal');
this.name = 'VoucherError';
}
}
function applyVoucher(amount, voucherCode) {
if (voucherCode === '') return amount;
if (voucherCode === 'HEMAT10') return amount - amount * VOUCHER_DISCOUNT;
throw new VoucherError(voucherCode);
}
function calculateTotal(cart, voucherCode) {
if (cart.length === 0) throw new Error('Keranjang kosong');
let total = calculateSubtotal(cart);
total = applyVoucher(total, voucherCode);
total = addShipping(total);
return 'Total: ' + formatRupiah(total);
}
function checkout(cart, voucherCode) {
try {
return calculateTotal(cart, voucherCode);
} catch (e) {
if (e instanceof VoucherError) return e.message + '. Cek lagi kodenya.';
throw e; // error lain, termasuk bug, dibiarkan naik
}
}
class VoucherError extends Erroradalah custom error: error buatan sendiri dengan nama sendiri.super(...)mengisimessage,this.namebikin console menulisVoucherError: .... Selaluthrow new ..., bukanthrow 'teks': objekErrormembawa stack trace, daftar baris tempat error terjadi.- Cek keranjang kosong ditaruh di baris pertama. Cek di awal fungsi seperti ini disebut guard clause: gagal secepatnya, sebelum hitungan apa pun jalan.
e instanceof VoucherErrormenangkap cuma error yang tahu cara menanganinya. Sisanya dilempar ulang denganthrow e.
> checkout(cartA, 'HEMAT01')
'Voucher HEMAT01 tidak dikenal. Cek lagi kodenya.'
> checkout([], '')
Uncaught Error: Keranjang kosong
Keranjang kosong sengaja dibiarkan naik: tombol bayar seharusnya gak bisa ditekan dengan keranjang kosong, jadi kalau sampai sini, itu bug di halamannya dan harus kelihatan.
Result object: gagal sebagai nilai biasa
Cara ketiga: kembalikan objek yang selalu bilang berhasil atau gagal, misalnya { ok: true, value: 220500 } atau { ok: false, error: 'Voucher HEMAT01 tidak dikenal' }. Pemanggil harus membaca ok dulu sebelum bisa memakai value, jadi lupa mengecek susah terjadi tanpa sadar.
| Cara | Cocok untuk | Bahayanya |
|---|---|---|
throw |
yang seharusnya gak terjadi: keranjang kosong sampai ke kasir, file config rusak | kalau ditangkap terlalu luas, ikut menelan bug |
| result object | gagal yang sering dan wajar: pembeli salah ketik voucher | lebih panjang ditulis |
return null |
"gak ketemu" yang normal, misalnya mencari produk, lalu langsung dicek | pemanggil yang lupa mengecek meneruskan null diam-diam |
Jangan menelan error
Menelan error artinya menangkapnya lalu melanjutkan seolah gak terjadi apa-apa. Contoh yang terlihat "aman":
function checkout(cart, voucherCode) {
try {
return calculateTotal(cart, voucherCode);
} catch (e) {
return calculateTotal(cart, ''); // "biar gak error": ulang tanpa voucher
}
}
Lalu seseorang salah ketik di applyVoucher: VOUCHER_DISCONT. Tiap HEMAT10 sekarang melempar ReferenceError, catch-nya menelan, dan kasir mengulang tanpa voucher:
> checkout(cartA, 'HEMAT10')
'Total: Rp 245.000'
Harusnya Rp 220.500. Semua pembeli HEMAT10 bayar penuh, gak ada error di mana pun. Dengan checkout versi instanceof di atas, typo yang sama langsung muncul di test pertama: Uncaught ReferenceError: VOUCHER_DISCONT is not defined.
Catch yang cuma mencatat lalu lanjut sama bahayanya:
function getTotal(cart, voucherCode) {
try {
return calculateTotal(cart, voucherCode);
} catch (e) {
console.error(e.message); // dicatat, lalu... lanjut
}
}
const total = getTotal(cartA, 'HEMAT01');
console.log(total.replace('Total: ', ''));
Voucher HEMAT01 tidak dikenal
Uncaught TypeError: Cannot read properties of undefined (reading 'replace')
Fungsi yang gak sampai ke return mengembalikan undefined, dan error-nya meledak di baris lain yang gak ada hubungannya. Fungsi async dengan await yang gagal juga sama persis. Catch yang baik: tangkap jenis yang spesifik, catat detailnya untuk developer (console.error(e)), tampilkan pesan ramah ke pembeli, dan jangan pernah tampilkan stack trace ke pengguna.
Latihan: divide
Di latihan, divide mengembalikan null saat membagi dengan nol. Buat class DivisionByZeroError extends Error dengan pesan Tidak bisa membagi dengan nol dan this.name = 'DivisionByZeroError'. divide(a, b) melempar throw new DivisionByZeroError() kalau b === 0, selain itu return a / b. safeDivide(a, b) memanggil divide di dalam try/catch: kalau error-nya instanceof DivisionByZeroError, kembalikan teks 'Error: ' + e.message, error lain dilempar ulang. Output-nya:
5
Error: Tidak bisa membagi dengan nol
5
⚠️ Jebakan yang sering ketemu
return nulltanpa dicek.nullikut hitungan sebagai 0:null + 15000jadi15000, tanpa error.- Catch yang menangkap semua. Ia ikut menelan bug, seperti typo
VOUCHER_DISCONTyang bikin semua HEMAT10 bayar penuh. Tangkap denganinstanceof, lempar ulang sisanya. - Catch yang cuma mencatat. Fungsinya mengembalikan
undefined, dan errornya pindah jadiTypeErrordi tempat lain. throw 'teks'. Teks gak membawa stack trace. Selaluthrow new Error(...)atau turunannya.- Di latihan, mencetak di dalam
safeDivide. KalausafeDividemencetak lalu gak mengembalikan apa-apa, baris kedua jadiundefined. Kembalikan teksnya.
TL;DR: Error handling yang baik bikin masalah terdengar dan sampai ke orang yang bisa memperbaikinya. Kasir yang diam bikin voucher salah ketik hilang dan keranjang kosong ditagih Rp 15.000. return null yang gak dicek bocor: null + 15000 jadi Total: Rp 15.000. throw dengan custom error (class VoucherError extends Error, this.name) dan guard clause di awal fungsi bikin masalah berhenti di tempatnya; pemanggil menangkap cuma instanceof VoucherError dan melempar ulang sisanya. Result object ({ ok, value, error }) cocok untuk gagal yang wajar. Jangan pernah menelan error: catch-all yang mengulang tanpa voucher menyembunyikan typo yang bikin semua HEMAT10 bayar Rp 245.000, dan catch yang cuma console.error bikin TypeError di tempat lain. Tangkap yang spesifik, log detailnya, pesan ramah ke pengguna. Di latihan: DivisionByZeroError, throw new, dan safeDivide mengembalikan Error: Tidak bisa membagi dengan nol.