Mengetahui sintaks try/except belum sama dengan tahu di mana harus menempatkannya. Kode yang menangkap exception di setiap lapisan justru sering lebih sulit di-debug daripada kode yang membiarkan error mengalir. Pada Chapter penutup Section ini kita akan menyusun strategi error handling: menentukan lapisan yang tepat untuk menangani, membangkitkan error sedini mungkin, tidak pernah menelan error, serta mencatat dan menerjemahkan error di batas antar-lapisan.
Tangani di Lapisan yang Bisa Bertindak #
Aturan utamanya sederhana: tangkap exception hanya di lapisan yang tahu respons terbaiknya. Lapisan lain cukup membiarkan exception naik ke pemanggil. Perhatikan tiga lapisan berikut:
def baca_stok(gudang, produk):
return gudang[produk]
def tampilkan_stok(gudang, produk):
print(f"Stok {produk}: {baca_stok(gudang, produk)}")
def main():
gudang = {"kopi": 12, "teh": 8}
try:
tampilkan_stok(gudang, "susu")
except KeyError:
print("Produk belum terdaftar, tampilkan form tambah produk")
main()Output yang Dihasilkan:
Produk belum terdaftar, tampilkan form tambah produkMari kita telaah apa yang terjadi pada baris-baris kode di atas:
- Baris 1–2 (
baca_stok): lapisan terbawah hanya membaca data. Function ini tidak tahu apa yang pantas dilakukan saat produk tidak ada — menampilkan pesan? membuat data? Karena itu, function ini tidak punyatrysama sekali. - Baris 4–5 (
tampilkan_stok): lapisan tengah juga tidak mengambil keputusan; exception hanya melewatinya. - Baris 9–12 (blok
trydimain()):KeyErrordaribaca_stok()naik dua lapisan dan baru ditangkap dimain()— satu-satunya tempat yang tahu aksi bisnis yang tepat: menampilkan form pendaftaran produk baru.
Alur keputusannya dirangkum diagram berikut:
Fail Fast #
Prinsip fail fast: laporkan kegagalan di momen paling awal, bukan biarkan data buruk menjalar. Konfigurasi yang kurang, misalnya, sebaiknya menolak aplikasi untuk menyala alih-alih membuatnya crash di tengah transaksi jam 3 pagi:
def muat_konfigurasi(konfigurasi):
for kunci in ("host", "port"):
if kunci not in konfigurasi:
raise ValueError(f"konfigurasi wajib: {kunci!r}")
print("Konfigurasi lengkap, aplikasi siap berjalan")
muat_konfigurasi({"host": "localhost", "port": 8000})
try:
muat_konfigurasi({"host": "localhost"})
except ValueError as e:
print("Aplikasi menolak start:", e)Output yang Dihasilkan:
Konfigurasi lengkap, aplikasi siap berjalan
Aplikasi menolak start: konfigurasi wajib: 'port'Pada pemanggilan pertama, seluruh kunci hadir sehingga validasi lolos. Pada pemanggilan kedua, kunci port hilang dan function muat_konfigurasi() membangkitkan ValueError saat itu juga — bukan saat koneksi database pertama gagal dua jam kemudian.
Jangan Menelan Error #
Pola paling berbahaya dalam penanganan error: except diikuti pass. Error tertangkap, tidak ada yang dikerjakan, tidak ada jejak yang tersisa:
def kirim_notifikasi_email(alamat):
raise ValueError(f"alamat email tidak valid: {alamat}")
def selesaikan_pesanan(pesanan):
try:
kirim_notifikasi_email(pesanan["email"])
except Exception:
pass
print(f"Pesanan {pesanan['id']} selesai")
selesaikan_pesanan({"id": 1024, "email": "[email protected]"})Output yang Dihasilkan:
Pesanan 1024 selesaiPesanan tampak selesai sempurna padahal notifikasi email tidak pernah terkirim. Bila pelanggan komplain “saya tidak menerima email”, tidak ada log, tidak ada traceback, tidak ada petunjuk apa pun — bug menjadi hantu yang tidak meninggalkan jejak.
Solusinya: catat kejadian dengan module logging, lalu putuskan kebijakannya — lanjut tanpa notifikasi, atau batalkan pesanan. Method logging.exception() otomatis menyertakan traceback dari exception yang sedang aktif. Simpan sebagai notifikasi.py, lalu jalankan dengan python3 notifikasi.py:
import logging
import sys
logging.basicConfig(
level=logging.INFO,
format="%(levelname)s: %(message)s",
stream=sys.stdout,
)
def kirim_notifikasi_email(alamat):
raise ValueError(f"alamat email tidak valid: {alamat}")
def selesaikan_pesanan(pesanan):
try:
kirim_notifikasi_email(pesanan["email"])
except ValueError:
logging.exception("Gagal kirim email untuk pesanan %s", pesanan["id"])
print(f"Pesanan {pesanan['id']} selesai")
selesaikan_pesanan({"id": 1024, "email": "[email protected]"})Output yang Dihasilkan:
ERROR: Gagal kirim email untuk pesanan 1024
Traceback (most recent call last):
File "notifikasi.py", line 15, in selesaikan_pesanan
kirim_notifikasi_email(pesanan["email"])
File "notifikasi.py", line 11, in kirim_notifikasi_email
raise ValueError(f"alamat email tidak valid: {alamat}")
ValueError: alamat email tidak valid: [email protected]
Pesanan 1024 selesaiSekarang keputusan bisnisnya tersampaikan jelas: notifikasi gagal dicatat lengkap dengan traceback, dan pesanan tetap dianggap selesai. Mari kita telaah bagian-bagian kuncinya pada notifikasi.py:
- Baris 4–8 (
logging.basicConfig(...)): konfigurasi level log dan format pesan disiapkan sekali di awal program. Parameterstream=sys.stdouthanya mengarahkan log agar mudah dilihat di contoh ini; secara default moduleloggingmenulis ke stderr. - Baris 17 (
logging.exception(...)): dipanggil dari dalam blokexcept, method ini mencatat pesan levelERRORbeserta traceback exception yang sedang aktif — tanpa perlu menyusun traceback manual. - Baris 18 (
print(f"Pesanan ...")): pesanan tetap dinyatakan selesai — keputusan sadar bahwa notifikasi bukan syarat keberhasilan pesanan, dan keputusan tersebut kini tercatat jejaknya.
Pembahasan logging secara menyeluruh menempuh Section Debugging dan Logging pada Chapter Module logging.
Mengubah Error Internal Menjadi Error Domain #
Lapisan paling luar sebuah API sebaiknya berbicara bahasa pemanggilnya, bukan bahasa implementasinya. Exception teknis diterjemahkan menjadi exception domain, dan rantai from e (Chapter Exception Chaining) menjaga jejak aslinya:
class DataPelangganError(Exception):
pass
def baca_usia(rekam):
try:
usia = int(rekam["usia"])
except KeyError as e:
raise DataPelangganError("kolom 'usia' hilang dari data") from e
except ValueError as e:
raise DataPelangganError(f"usia harus angka: {rekam['usia']!r}") from e
return usia
def tampilkan_profil(rekam):
try:
print("Usia pelanggan:", baca_usia(rekam))
except DataPelangganError as e:
print("Data pelanggan bermasalah:", e)
print("Penyebab asli :", repr(e.__cause__))
tampilkan_profil({"nama": "Budi", "usia": "tujuh belas"})Output yang Dihasilkan:
Data pelanggan bermasalah: usia harus angka: 'tujuh belas'
Penyebab asli : ValueError("invalid literal for int() with base 10: 'tujuh belas'")Function tampilkan_profil() bekerja dengan satu jenis kegagalan domain — DataPelangganError — tanpa perlu tahu bahwa di dalamnya ada KeyError dan ValueError. Detail teknis tetap bisa ditelusuri lewat __cause__ saat debugging.
Checklist Kualitas Produksi #
| Pertanyaan pemeriksaan | Jawaban yang sehat |
|---|---|
| Exception apa saja yang masuk akal ditangkap di sini? | hanya yang punya rencana pemulihan konkret |
| Apa yang dilakukan handler saat error? | pulihkan, catat log, atau terjemahkan — bukan pass |
| Apakah pesan error membantu debugging? | menyebut nilai, kunci, atau ID yang bermasalah |
| Apakah error internal bocor ke pengguna akhir? | tidak — diterjemahkan menjadi error domain di batas API |
| Apakah kegagalan terdeteksi sedini mungkin? | validasi dilakukan di pintu masuk data |
| Apakah log tersedia dengan traceback lengkap? | ya, melalui logging.exception() |
Apakah Ctrl+C dan sys.exit() masih bekerja? | ya — handler tidak menangkap BaseException |
Tujuh butir pada tabel tersebut adalah pertanyaan pemeriksaan diri sebelum kode berangkat ke produksi. Bersama seluruh Chapter pada Section ini — dari anatomi exception sampai pohon hierarki class — kita kini punya bekal lengkap untuk menangani error secara sadar: tahu kapan menangkap, tahu kapan melepas, dan tahu kapan bersuara.