Saat program gagal, interpreter Python tidak hanya berhenti diam; interpreter Python mencetak laporan lengkap bernama traceback ke stderr. Bagi pemula, laporan tersebut sering terlihat seperti dinding teks yang menakutkan. Padahal traceback justru berisi informasi paling berharga untuk debugging: jalur eksekusi yang ditempuh, baris persis yang gagal, dan jenis error yang terjadi. Pada Chapter ini kita akan mempelajari cara membaca traceback dengan urutan yang benar, mengenali error yang paling sering muncul, dan menafsirkan traceback berantai.

Urutan Membaca: dari Bawah ke Atas #

Kebiasaan pertama yang perlu dibangun: baca traceback dari baris paling bawah. Baris terakhir selalu berisi nama exception dan pesan errornya, sedangkan baris-baris di atasnya berisi jalur panggilan yang menuju ke titik kegagalan. Simpan kode berikut sebagai kasir.py, lalu jalankan dengan python3 kasir.py:

python
def hitung_harga_satuan(total, jumlah):
    return total / jumlah


def proses_pesanan(total, jumlah):
    return hitung_harga_satuan(total, jumlah)


print(proses_pesanan(150000, 0))

Output yang Dihasilkan (dicetak interpreter Python ke stderr):

text
Traceback (most recent call last):
  File "kasir.py", line 9, in <module>
    print(proses_pesanan(150000, 0))
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "kasir.py", line 6, in proses_pesanan
    return hitung_harga_satuan(total, jumlah)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "kasir.py", line 2, in hitung_harga_satuan
    return total / jumlah
           ~~~~~~^~~~~~~~
ZeroDivisionError: division by zero

Alur membacanya ada dua arah. Untuk mengetahui error apa, baca dari bawah: baris terakhir menyebut ZeroDivisionError: division by zero. Untuk mengetahui bagaimana sampai di situ, baca dari atas ke bawah: modul utama memanggil proses_pesanan() di baris 9, proses_pesanan() memanggil hitung_harga_satuan() di baris 6, lalu pembagian di baris 2 gagal. Setiap entri File ... seperti itu disebut satu frame, dan anatomi setiap frame selalu sama:

Bagian frameMakna
File "kasir.py", line 2, in hitung_harga_satuannama file, nomor baris, dan function yang sedang berjalan
return total / jumlahisi baris sumber yang sedang dieksekusi
~~~~~~^~~~~~~~penanda yang menyorot bagian ekspresi yang memicu error

Pada contoh di atas, tanda ~ dan ^ menunjuk operator / beserta kedua operannya — sinyal kuat bahwa pembagian di baris tersebut adalah biang keladi, bukan kode di function pemanggilnya.

Tiga Error yang Sering Muncul #

Sebagian besar traceback harian berasal dari segelintir exception yang sama. Tiga contoh berikut mewakili pola yang paling sering dijumpai. Pertama, KeyError saat kunci dictionary tidak ada — simpan sebagai laporan.py:

python
penjualan = {"teh": 120, "kopi": 85}


def ambil_penjualan(produk):
    return penjualan[produk]


print(ambil_penjualan("susu"))

Output yang Dihasilkan (dicetak interpreter Python ke stderr):

text
Traceback (most recent call last):
  File "laporan.py", line 8, in <module>
    print(ambil_penjualan("susu"))
          ^^^^^^^^^^^^^^^^^^^^^^^
  File "laporan.py", line 5, in ambil_penjualan
    return penjualan[produk]
           ~~~~~~~~~^^^^^^^^
KeyError: 'susu'

Kedua, TypeError saat tipe data tidak cocok — simpan sebagai struk.py:

python
def format_label(nama, jumlah):
    return nama + " x " + jumlah


print(format_label("Kopi Gayo", 3))

Output yang Dihasilkan (dicetak interpreter Python ke stderr):

text
Traceback (most recent call last):
  File "struk.py", line 5, in <module>
    print(format_label("Kopi Gayo", 3))
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "struk.py", line 2, in format_label
    return nama + " x " + jumlah
           ~~~~~~~~~~~~~^~~~~~~~
TypeError: can only concatenate str (not "int") to str

Pesan can only concatenate str (not "int") to str langsung memberi tahu object mana yang salah tipe: nilai 3 bertipe int padahal operator + pada string hanya menerima string. Solusinya adalah konversi dengan str(jumlah) atau berpindah ke f-string. Ketiga, ZeroDivisionError seperti pada kasir.py di atas. Ketiganya mengikuti pola yang sama: baris terakhir menyebut jenis kesalahan, penanda ~/^ menunjuk operasi yang gagal, dan sisanya memetakan jalur panggilannya.

Traceback Berantai #

Kadang satu error terjadi saat interpreter Python sedang menangani error lain. Hasilnya adalah dua traceback yang tersambung. Simpan kode berikut sebagai gudang.py:

python
stok = {"teh": 12}

try:
    jumlah = stok["kopi"]
except KeyError:
    print(f"Stok kopi habis: {jumlah}")

Output yang Dihasilkan (dicetak interpreter Python ke stderr):

text
Traceback (most recent call last):
  File "gudang.py", line 4, in <module>
    jumlah = stok["kopi"]
             ~~~~^^^^^^^^
KeyError: 'kopi'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "gudang.py", line 6, in <module>
    print(f"Stok kopi habis: {jumlah}")
                              ^^^^^^
NameError: name 'jumlah' is not defined

Frasa During handling of the above exception berarti error kedua (NameError) lahir di dalam blok except yang sedang menangani error pertama (KeyError) — penugasan jumlah tidak pernah terjadi karena baris 4 gagal lebih dulu.

Bentuk kedua adalah rantai eksplisit lewat raise ... from e. Simpan berikut sebagai pembayaran.py:

python
class DataPesananError(Exception):
    pass


def muat_total(teks):
    try:
        return int(teks)
    except ValueError as e:
        raise DataPesananError(f"total tidak valid: {teks!r}") from e


print(muat_total("lima ribu"))

Output yang Dihasilkan (dicetak interpreter Python ke stderr):

text
Traceback (most recent call last):
  File "pembayaran.py", line 7, in muat_total
    return int(teks)
           ^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'lima ribu'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "pembayaran.py", line 12, in <module>
    print(muat_total("lima ribu"))
          ^^^^^^^^^^^^^^^^^^^^^^^
  File "pembayaran.py", line 9, in muat_total
    raise DataPesananError(f"total tidak valid: {teks!r}") from e
DataPesananError: total tidak valid: 'lima ribu'

Frasa The above exception was the direct cause of the following exception menyatakan hubungan yang disengaja: error ValueError di atas sengaja dibungkus menjadi DataPesananError yang lebih bermakna bagi pemanggilnya, dan klausa from e menjadikan e sebagai penyebab resmi. Mekanisme lengkap kedua bentuk rantai ini sudah kita bedah tuntas pada Chapter Exception Chaining.

Daftar Periksa Membaca Traceback #

Saat menatap traceback, jalankan urutan pemeriksaan berikut:

  1. Baca baris paling bawah: exception apa dan apa pesannya?
  2. Baca frame paling bawah: file dan baris mana yang gagal?
  3. Amati penanda ~ dan ^: bagian ekspresi mana yang disorot?
  4. Baca frame berikutnya ke arah atas bila perlu: siapa pemanggilnya?
  5. Buka file pada baris tersebut dan periksa nilai variabel di sekitarnya.

Dengan kemampuan membaca traceback, langkah berikutnya adalah memilih alat bantu: kapan cukup memakai print(), dan kapan saatnya naik kelas ke debugger interaktif.