ValueError dan KeyError menjelaskan kegagalan teknis, tetapi tidak menjelaskan kegagalan bisnis. Pesan “saldo tidak cukup” atau “pesanan sudah dikirim” adalah kegagalan domain yang layak punya nama sendiri agar kode pembaca bisa menangkapnya secara presisi. Python membolehkan kita mendefinisikan class exception sendiri. Pada Chapter ini kita akan membuat class exception pertama, menambahkan atribut kustom, lalu merancang hierarki error untuk satu aplikasi.

Materi ini memakai sintaks class di tingkat paling dasar. Pembahasan class secara menyeluruh — instance, method, inheritance — menempuh Section Object-Oriented Programming mulai dari Chapter Memahami Class dan Object.

Class Exception Pertama #

Resep minimalnya singkat: definisikan class yang mewarisi Exception, isi body dengan pass, selesai:

python
class SaldoTidakCukupError(Exception):
    pass

def tarik_saldo(saldo, jumlah):
    if jumlah > saldo:
        raise SaldoTidakCukupError(
            f"saldo {saldo} tidak cukup untuk penarikan {jumlah}"
        )
    return saldo - jumlah

try:
    tarik_saldo(50000, 100000)
except SaldoTidakCukupError as e:
    print("Transaksi gagal:", e)

Output yang Dihasilkan:

text
Transaksi gagal: saldo 50000 tidak cukup untuk penarikan 100000

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

  1. Baris 1 (class SaldoTidakCukupError(Exception):): class baru didefinisikan sebagai turunan Exception. Dengan mewarisi Exception, class ini otomatis bisa dibangkitkan raise, dicetak pesannya, dan ditangkap klausa except.
  2. Baris 2 (pass): body class tidak butuh isi tambahan; seluruh perilaku exception diwarisi dari Exception.
  3. Baris 6–8 (raise SaldoTidakCukupError(...)): instance dibuat dengan satu argumen string, lalu dibangkitkan saat jumlah penarikan melebihi saldo.
  4. Baris 13 (except SaldoTidakCukupError as e:): handler menangkap class buatan sendiri persis seperti class bawaan.

Menyimpan Data Tambahan dengan init #

Pesan string saja kadang kurang. Program penanganan pesanan butuh tahu ID pesanan mana yang bermasalah — bukan sekadar kalimat errornya. Method __init__ mengisi kebutuhan itu:

python
class PesananError(Exception):
    def __init__(self, pesanan_id, pesan):
        super().__init__(pesan)
        self.pesanan_id = pesanan_id

def batalkan_pesanan(status):
    raise PesananError(1024, f"pesanan berstatus {status} tidak bisa dibatalkan")

try:
    batalkan_pesanan("dikirim")
except PesananError as e:
    print("Pesan      :", e)
    print("ID pesanan :", e.pesanan_id)
    print("Tipe       :", type(e).__name__)

Output yang Dihasilkan:

text
Pesan      : pesanan berstatus dikirim tidak bisa dibatalkan
ID pesanan : 1024
Tipe       : PesananError

Dua keputusan penting ada pada __init__:

  1. super().__init__(pesan): constructor class induk dipanggil dengan pesan, sehingga mekanisme bawaan Exception (atribut args sampai hasil str(e)) tetap bekerja normal. Membaca e tetap menghasilkan kalimat pesan yang rapi.
  2. self.pesanan_id = pesanan_id: data tambahan disimpan sebagai atribut instance. Pemanggil yang menangkap exception bisa membaca e.pesanan_id tanpa mem-parsing string pesan.

Mekanisme __init__, super(), dan atribut instance akan dibedah tuntas pada Chapter Constructor dan init — pada titik ini kita cukup memakai polanya.

Merancang Hierarki Error Aplikasi #

Aplikasi nyata punya banyak jenis kegagalan. Praktik yang matang: sediakan satu class akar untuk seluruh error aplikasi, lalu turunkan class spesifik darinya. Pemanggil bisa memilih presisi tangkapan sesuai kebutuhannya:

python
class AplikasiError(Exception):
    pass

class PembayaranError(AplikasiError):
    pass

class SaldoTidakCukupError(PembayaranError):
    pass

class KartuKedaluwarsaError(PembayaranError):
    pass

def bayar(saldo, total):
    if saldo < total:
        raise SaldoTidakCukupError(f"saldo {saldo}, total tagihan {total}")

def checkout(saldo, total):
    try:
        bayar(saldo, total)
        print("Pembayaran berhasil")
    except SaldoTidakCukupError as e:
        print("Tawarkan top-up saldo:", e)
    except PembayaranError as e:
        print("Tampilkan halaman error pembayaran:", e)
    except AplikasiError as e:
        print("Tampilkan error umum aplikasi:", e)

checkout(50000, 80000)

Output yang Dihasilkan:

text
Tawarkan top-up saldo: saldo 50000, total tagihan 80000

Struktur pewarisan di atas membentuk pohon: AplikasiError menjadi akar, PembayaranError menengahi kegagalan pembayaran, dan SaldoTidakCukupError menandai kasus spesifik. Aturan sub-class yang kita pelajari pada Chapter Menangani Beberapa Exceptions membuat fungsi checkout() bisa memilih — menangkap SaldoTidakCukupError untuk menawarkan top-up, atau cukup PembayaranError untuk semua masalah pembayaran sekaligus, atau AplikasiError sebagai jaring pengaman menyeluruh. Handler except SaldoTidakCukupError di contoh menangkap SaldoTidakCukupError beserta seluruh turunannya kelak.

Catatan tentang str dan repr #

Class Exception sudah menyediakan __str__ yang mengembalikan isi args — kita tidak perlu menulisnya sendiri. Method __repr__ bawaan pun sudah menghasilkan bentuk NamaClass('pesan') yang rapi:

python
class SaldoError(Exception):
    pass

err = SaldoError("saldo tidak cukup")

print(err)
print(repr(err))

Output yang Dihasilkan:

text
saldo tidak cukup
SaldoError('saldo tidak cukup')

Mendefinisikan ulang __str__ hanya perlu bila class Anda mengubah cara pesan dibentuk. Untuk kasus umum — pesan utuh di argumen pertama — implementasi bawaan sudah memadai, dan menimpanya tanpa alasan kuat justru menghilangkan konsistensi dengan exception bawaan Python.

Satu pertanyaan tersisa setelah exception baru dibangkitkan dari dalam blok except: bagaimana menjaga jejak error asli? Jawabannya ada pada teknik chaining di Chapter Exception Chaining.