Membungkus error bawaan menjadi error yang lebih bermakna adalah praktik yang baik — sampai informasi aslinya ikut terbuang. Alih-alih tahu bahwa KeyError: 'port' terjadi di dalam baca_port(), pemanggil hanya menerima ValueError tanpa jejak akar masalah. Python menjawab ini dengan exception chaining: setiap exception bisa menyimpan exception lain sebagai riwayatnya. Pada Chapter ini kita akan membedah rantai implisit lewat __context__, rantai eksplisit lewat __cause__, dan cara menyembunyikan konteks dengan from None.
Rantai Implisit dengan context #
Ketika exception baru dibangkitkan ketika interpreter Python sedang menangani exception lain, exception yang lama otomatis tersimpan pada atribut __context__ milik exception yang baru. Mekanisme ini bekerja tanpa perintah tambahan apa pun. Simpan sebagai pengaturan.py, lalu jalankan dengan python3 pengaturan.py:
def muat_pengaturan(data):
try:
return data["port"]
except KeyError:
return data["port_default"]
pengaturan = {"host": "localhost"}
print(muat_pengaturan(pengaturan))Output yang Dihasilkan (dicetak interpreter Python ke stderr):
Traceback (most recent call last):
File "pengaturan.py", line 3, in muat_pengaturan
return data["port"]
~~~~^^^^^^^^
KeyError: 'port'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "pengaturan.py", line 8, in <module>
print(muat_pengaturan(pengaturan))
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "pengaturan.py", line 5, in muat_pengaturan
return data["port_default"]
~~~~^^^^^^^^^^^^^^^^
KeyError: 'port_default'Bacalah traceback dari bawah ke atas untuk memahami kronologinya. Pemanggilan data["port"] membangkitkan KeyError: 'port'. Di dalam handler-nya, akses data["port_default"] juga gagal — kunci ini pun tidak ada — sehingga exception kedua terjadi. Kalimat During handling of the above exception, another exception occurred: adalah penanda rantai implisit: error kedua terjadi tidak sengaja di tengah penanganan error pertama.
Mari kita telaah kronologi pembentukan rantai tersebut pada kode:
- Baris 3 (
return data["port"]): dictionarypengaturantidak memiliki kunci"port", sehinggaKeyErrordibangkitkan dan handlerexcept KeyError:aktif. - Baris 5 (
return data["port_default"]): baris ini berada DI DALAM handler. Akses kunci"port_default"juga gagal, sehinggaKeyErrorkedua lahir di tengah penangananKeyErrorpertama. - Rantai tersimpan otomatis: interpreter Python menyimpan
KeyError: 'port'ke atribut__context__milikKeyError: 'port_default'sebelum meneruskannya keluar — tanpa satu baris kode pun dari kita.
Atribut tersebut bisa kita periksa langsung saat exception tertangkap:
try:
try:
{}["kunci"]
except KeyError:
raise ValueError("konfigurasi tidak lengkap")
except ValueError as e:
print("Exception baru :", e)
print("__context__ :", repr(e.__context__))
print("__cause__ :", e.__cause__)Output yang Dihasilkan:
Exception baru : konfigurasi tidak lengkap
__context__ : KeyError('kunci')
__cause__ : None__context__ berisi KeyError lama secara otomatis, sedangkan __cause__ masih None — kita belum menyatakan hubungan sebab-akibat secara eksplisit.
Rantai Eksplisit dengan cause #
Bentuk raise ... from e menyatakan hubungan sebab-akibat secara sadar: exception baru memang dibangkitkan karena exception lama. Interpreter Python menyimpan e ke atribut __cause__. Simpan sebagai server.py dan jalankan:
def baca_port(data):
try:
return int(data["port"])
except KeyError as e:
raise ValueError("kunci 'port' wajib diisi") from e
pengaturan = {"host": "localhost"}
print(baca_port(pengaturan))Output yang Dihasilkan (dicetak interpreter Python ke stderr):
Traceback (most recent call last):
File "server.py", line 3, in baca_port
return int(data["port"])
~~~~^^^^^^^^
KeyError: 'port'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "server.py", line 8, in <module>
print(baca_port(pengaturan))
^^^^^^^^^^^^^^^^^^^^^
File "server.py", line 5, in baca_port
raise ValueError("kunci 'port' wajib diisi") from e
ValueError: kunci 'port' wajib diisiPerhatikan perbedaan kalimat penghubungnya: The above exception was the direct cause of the following exception: — bukan lagi during handling, melainkan hubungan sebab langsung yang dinyatakan programmer. Perbedaan ini juga terlihat pada traceback sebelumnya di Chapter raise: rantai eksplisit menyampaikan “error baru sengaja dibangkitkan karena error lama”.
Atribut __cause__ bisa dibaca program seperti biasa:
def baca_port(data):
try:
return int(data["port"])
except KeyError as e:
raise ValueError("kunci 'port' wajib diisi") from e
try:
baca_port({"host": "localhost"})
except ValueError as e:
print("Pesan :", e)
print("__cause__ :", repr(e.__cause__))Output yang Dihasilkan:
Pesan : kunci 'port' wajib diisi
__cause__ : KeyError('port')Menyembunyikan Konteks dengan from None #
Bentuk raise ... from None memutus rantai secara sengaja. Atribut __context__ tetap terisi, tetapi atribut __suppress_context__ bernilai True sehingga traceback tidak menampilkan konteks lama:
def set_level(nilai):
try:
return int(nilai)
except ValueError:
raise ValueError(f"level harus berupa angka, diterima: {nilai!r}") from None
try:
set_level("tinggi")
except ValueError as e:
print("__context__ :", repr(e.__context__))
print("__cause__ :", repr(e.__cause__))
print("__suppress_context__:", e.__suppress_context__)Output yang Dihasilkan:
__context__ : ValueError("invalid literal for int() with base 10: 'tinggi'")
__cause__ : None
__suppress_context__: TrueKapan from None pantas dipakai? Ketika konteks internal justru menyesatkan. Contoh di atas mengganti pesan teknis invalid literal for int() dengan pesan domain yang sudah menjelaskan nilai apa yang ditolak; menampilkan keduanya hanya menambah kebisingan.
Membaca dan Memakai Rantai #
Tiga atribut bekerja sama dan dirangkum pada tabel berikut:
| Atribut | Cara terisi | Makna bagi pembaca traceback |
|---|---|---|
__context__ | otomatis, saat exception baru muncul saat handler aktif | “kebetulan terjadi saat menangani error lain” |
__cause__ | hanya lewat raise ... from e | “dibangkitkan justru karena error lain” |
__suppress_context__ | True setelah from None | konteks lama disembunyikan dari traceback |
Bagi debugging, rantai menjawab dua pertanyaan sekaligus: gejala apa yang sampai ke pemanggil (exception paling bawah) dan akar masalahnya apa (exception paling atas). Semakin panjang lapisan aplikasi, semakin berharga rantai yang utuh — memotongnya sama dengan membuang alibi pertama dari daftar tersangka. Alur kerja praktisnya: baca dulu exception baris terakhir untuk tahu gejala, lalu naik ke bagian atas traceback untuk melihat akar masalah yang menimbulkannya.
Kapan memakai bentuk yang mana? from e saat menerjemahkan exception di batas antar-lapisan — misalnya mengubah KeyError teknis menjadi KonfigurasiError domain. from None saat pesan baru sudah menceritakan semuanya dan detail internal hanya akan mengarahkan pembaca ke arah yang salah. Tanpa keduanya pun, __context__ tetap mencatat kejadian secara jujur — hanya saja hubungan sebab-akibatnya belum dinyatakan secara eksplisit.
Rantai menghubungkan exception satu dengan lainnya, tetapi seluruh class exception juga terhubung dalam satu pohon keluarga. Peta lengkapnya akan kita telusuri pada Chapter Exception Hierarchy.