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:

python
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):

text
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:

  1. Baris 3 (return data["port"]): dictionary pengaturan tidak memiliki kunci "port", sehingga KeyError dibangkitkan dan handler except KeyError: aktif.
  2. Baris 5 (return data["port_default"]): baris ini berada DI DALAM handler. Akses kunci "port_default" juga gagal, sehingga KeyError kedua lahir di tengah penanganan KeyError pertama.
  3. Rantai tersimpan otomatis: interpreter Python menyimpan KeyError: 'port' ke atribut __context__ milik KeyError: 'port_default' sebelum meneruskannya keluar — tanpa satu baris kode pun dari kita.

Atribut tersebut bisa kita periksa langsung saat exception tertangkap:

python
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:

text
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:

python
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):

text
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 diisi

Perhatikan 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:

python
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:

text
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:

python
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:

text
__context__         : ValueError("invalid literal for int() with base 10: 'tinggi'")
__cause__           : None
__suppress_context__: True

Kapan 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:

AtributCara terisiMakna 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 Nonekonteks 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.