kodingup.camp

Beranda/JavaScript Mastery: Prototype, Class, Waktu, Error & Async

Dasar Penanganan Error

Modul 10 dari 153 menit bacaException Management Fundamentals

Isi modul

    Kenapa ini penting

    Program yang bagus bukan program yang tidak pernah error, melainkan program yang tahu harus berbuat apa saat error terjadi. Input pengguna bisa salah, server bisa mati, file bisa tidak ada, JSON dari API bisa rusak. Tanpa penanganan error, satu kesalahan kecil membuat seluruh aplikasi berhenti, layar putih, dan pengguna bingung.

    Bayangkan kamu kasir di toko. Ketika mesin EDC gagal, kamu tidak meninggalkan toko. Kamu menawarkan pembayaran tunai, mencatat kejadiannya, lalu melanjutkan pelayanan. Itulah try/catch: mencoba, menangkap masalah, dan membereskan (finally). Di modul ini kamu juga belajar melempar error sendiri dengan pesan yang jelas, dan membuat custom error class supaya jenis kesalahan bisa dibedakan (misalnya "saldo tidak cukup" berbeda penanganannya dengan "koneksi terputus").

    Konsep inti

    try, catch, finally

    try {
      // kode yang mungkin gagal
    } catch (error) {
      // dijalankan HANYA jika try melempar error
    } finally {
      // SELALU dijalankan, error atau tidak (bahkan setelah return)
    }
    

    throw

    throw menghentikan eksekusi function saat ini dan "melempar" nilai ke atas sampai ada catch yang menangkap. Jika tidak ada sama sekali, program berhenti (di Node.js) atau muncul Uncaught Error di console (browser).

    Kamu bisa melempar apa saja (throw "gagal"), tetapi selalu lempar object Error, karena hanya Error yang membawa stack (jejak lokasi kejadian) dan bisa dibedakan dengan instanceof.

    Objek Error dan jenis bawaan

    Setiap error punya:

    Jenis Kapan muncul Contoh
    Error Umum throw new Error("...")
    TypeError Operasi pada tipe yang salah undefined.nama, memanggil bukan function
    ReferenceError Variabel tidak ada console.log(xyz)
    RangeError Angka di luar rentang new Array(-1), toFixed(200)
    SyntaxError Kode/JSON tidak valid JSON.parse("{rusak")

    Custom error class

    Dengan class ... extends Error, kamu membuat jenis error sendiri yang membawa data tambahan:

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

    Lalu di catch, bedakan dengan instanceof dan tangani sesuai jenisnya. Error yang tidak kamu kenali sebaiknya dilempar ulang (throw err), jangan ditelan diam-diam.

    Kapan memakai try/catch?

    Bukan di setiap baris. Tangkap error di tempat kamu bisa melakukan sesuatu terhadapnya: menampilkan pesan ke pengguna, mencoba ulang, memakai nilai cadangan. Validasi yang bisa dicek dengan if (misalnya "apakah input kosong?") lebih baik dicek langsung sebelum menjadi error.

    Contoh kode

    // 1. try/catch/finally dasar
    function parseKonfigurasi(teks) {
      try {
        const cfg = JSON.parse(teks);
        console.log("Konfigurasi terbaca:", cfg.tema);
        return cfg;
      } catch (err) {
        console.log(`Gagal membaca (${err.name}): pakai default`);
        return { tema: "terang" };
      } finally {
        console.log("Selesai memproses konfigurasi");
      }
    }
    parseKonfigurasi('{"tema":"gelap"}');
    // → Konfigurasi terbaca: gelap
    // → Selesai memproses konfigurasi
    parseKonfigurasi("{rusak");
    // → Gagal membaca (SyntaxError): pakai default
    // → Selesai memproses konfigurasi
    
    // 2. Custom error class dengan data tambahan
    class AppError extends Error {
      constructor(message, options) {
        super(message, options); // options bisa berisi { cause }
        this.name = this.constructor.name; // otomatis sesuai nama class turunan
      }
    }
    class ValidasiError extends AppError {
      constructor(field, message) {
        super(message);
        this.field = field;
      }
    }
    class SaldoTidakCukupError extends AppError {
      constructor(saldo, diminta) {
        super(`Saldo Rp${saldo} tidak cukup untuk Rp${diminta}`);
        this.kekurangan = diminta - saldo;
      }
    }
    
    // 3. Melempar error dengan pesan jelas
    function transfer(saldo, jumlah, tujuan) {
      if (!tujuan) throw new ValidasiError("tujuan", "Rekening tujuan wajib diisi");
      if (!Number.isFinite(jumlah) || jumlah <= 0) {
        throw new ValidasiError("jumlah", "Jumlah harus angka positif");
      }
      if (jumlah > saldo) throw new SaldoTidakCukupError(saldo, jumlah);
      return saldo - jumlah;
    }
    
    // 4. Menangani per jenis error
    function prosesTransfer(saldo, jumlah, tujuan) {
      try {
        const sisa = transfer(saldo, jumlah, tujuan);
        return `Berhasil. Sisa saldo Rp${sisa}`;
      } catch (err) {
        if (err instanceof ValidasiError) return `Input salah di "${err.field}": ${err.message}`;
        if (err instanceof SaldoTidakCukupError) return `${err.message} (kurang Rp${err.kekurangan})`;
        throw err; // error tak dikenal: jangan ditelan!
      }
    }
    console.log(prosesTransfer(100000, 50000, "123"));  // → Berhasil. Sisa saldo Rp50000
    console.log(prosesTransfer(100000, -5, "123"));     // → Input salah di "jumlah": Jumlah harus angka positif
    console.log(prosesTransfer(100000, 150000, "123")); // → Saldo Rp100000 tidak cukup untuk Rp150000 (kurang Rp50000)
    console.log(prosesTransfer(100000, 1000, ""));      // → Input salah di "tujuan": Rekening tujuan wajib diisi
    
    // 5. Properti error
    try {
      transfer(0, 10, "x");
    } catch (e) {
      console.log(e.name, "|", e instanceof AppError, "|", e instanceof Error);
      // → SaldoTidakCukupError | true | true
    }
    
    // 6. Membungkus error dengan cause (menjaga jejak aslinya)
    function muatProfil(json) {
      try {
        return JSON.parse(json);
      } catch (err) {
        throw new AppError("Profil pengguna rusak", { cause: err });
      }
    }
    try {
      muatProfil("{nama:");
    } catch (e) {
      console.log(e.message, "← penyebab:", e.cause.name); // → Profil pengguna rusak ← penyebab: SyntaxError
    }
    
    // 7. finally tetap jalan meski ada return
    function cobaFinally() {
      try {
        return "dari try";
      } finally {
        console.log("finally tetap jalan");
      }
    }
    console.log(cobaFinally());
    // → finally tetap jalan
    // → dari try
    
    // 8. Pola "hasil atau error" tanpa melempar (untuk validasi massal)
    function validasiTransaksi(t) {
      const masalah = [];
      if (!t.keterangan) masalah.push("keterangan kosong");
      if (typeof t.jumlah !== "number") masalah.push("jumlah bukan angka");
      return masalah.length ? { ok: false, masalah } : { ok: true };
    }
    console.log(validasiTransaksi({ keterangan: "", jumlah: "10" }));
    // → { ok: false, masalah: [ 'keterangan kosong', 'jumlah bukan angka' ] }
    

    Perhatikan trik this.name = this.constructor.name di AppError. Semua turunannya otomatis punya name yang benar tanpa perlu menulis ulang. Pesan error yang baik menjawab tiga hal: apa yang salah, di mana, dan kalau bisa bagaimana memperbaikinya.

    Kesalahan umum

    1. Menelan error diam-diam. catch (e) {} kosong menyembunyikan bug. Minimal catat (console.error(e)), atau lempar ulang error yang tidak kamu kenali.
    2. Melempar string. throw "gagal" tidak punya stack dan tidak bisa dicek dengan instanceof. Selalu throw new Error("gagal").
    3. Lupa super(message) di custom error. Tanpa itu, message kosong dan this tidak bisa dipakai.
    4. Mengira try/catch menangkap error di callback timer. try { setTimeout(() => { throw ... }) } catch {} tidak menangkap apa-apa, karena callback berjalan nanti setelah blok try selesai. Pasang try/catch di dalam callback (atau pakai Promise/async, dibahas berikutnya).
    5. Membungkus seluruh program dalam satu try/catch raksasa. Kamu kehilangan konteks. Tangkap di level yang bisa memberi respons berarti.

    Latihan

    1. Mudah, Tulis bagi(a, b) yang melempar RangeError jika b bernilai 0. Panggil di dalam try/catch dan cetak pesannya.
    2. Sedang, Buat TidakDitemukanError extends Error dengan properti id. Tulis cariProduk(daftar, id) yang melemparnya jika produk tidak ada.
    3. Menantang, Tulis parseAngkaRupiah(teks) yang mengubah "Rp1.250.000" menjadi 1250000. Lempar ValidasiError (dengan cause jika ada error dalam) untuk input seperti "Rp1,2jt". Uji tiga input.
    Lihat solusi
    // 1
    function bagi(a, b) {
      if (b === 0) throw new RangeError("Pembagi tidak boleh nol");
      return a / b;
    }
    try { bagi(10, 0); } catch (e) { console.log(e.name, e.message); } // → RangeError Pembagi tidak boleh nol
    
    // 2
    class TidakDitemukanError extends Error {
      constructor(id) {
        super(`Produk dengan id ${id} tidak ditemukan`);
        this.name = "TidakDitemukanError";
        this.id = id;
      }
    }
    function cariProduk(daftar, id) {
      const p = daftar.find((x) => x.id === id);
      if (!p) throw new TidakDitemukanError(id);
      return p;
    }
    try { cariProduk([{ id: 1, nama: "Teh" }], 7); }
    catch (e) { if (e instanceof TidakDitemukanError) console.log(e.message); } // → Produk dengan id 7 tidak ditemukan
    
    // 3
    class ValidasiError extends Error {
      constructor(message, options) { super(message, options); this.name = "ValidasiError"; }
    }
    function parseAngkaRupiah(teks) {
      const bersih = String(teks).replace(/^Rp\s?/, "").replaceAll(".", "");
      if (!/^\d+$/.test(bersih)) throw new ValidasiError(`Format rupiah tidak valid: "${teks}"`);
      return Number(bersih);
    }
    for (const input of ["Rp1.250.000", "Rp 75.000", "Rp1,2jt"]) {
      try { console.log(parseAngkaRupiah(input)); }
      catch (e) { console.log(`${e.name}: ${e.message}`); }
    }
    // → 1250000
    // → 75000
    // → ValidasiError: Format rupiah tidak valid: "Rp1,2jt"
    

    Ringkasan

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.