Setiap program yang serius pasti ketemu error di runtime. File konfigurasi hilang, API eksternal timeout, user mengetik huruf di kolom yang seharusnya angka. Kalau dibiarkan, Python langsung berhenti dan melempar traceback panjang yang bikin user bingung. Tapi kamu bisa menangkap error itu, meresponsnya dengan tenang, dan menjaga aplikasi tetap jalan. Di artikel ini saya tunjukkan caranya, dari pola paling dasar sampai custom exception yang rapi — semua kodenya bisa kamu salin dan jalankan.
Dasar: try dan except
Pola paling sederhana adalah membungkus kode yang berpotensi gagal di blok try, lalu menangani kegagalannya di blok except. Contoh klasik: membaca file JSON yang mungkin belum ada.
import json
def muat_konfigurasi(path: str) -> dict:
try:
with open(path) as f:
return json.load(f)
except FileNotFoundError:
print(f"File {path} tidak ditemukan, pakai konfigurasi default")
return {}
config = muat_konfigurasi("config.json")
print(config)
Kalau config.json ada dan valid, fungsi mengembalikan isinya. Kalau tidak, blok except FileNotFoundError jalan dan fungsi mengembalikan dict kosong. Program tidak crash, dan sisa kode tetap dieksekusi. Itu inti dari exception handling: kegagalan jadi sesuatu yang bisa kamu kontrol, bukan bencana.
Tangkap Exception yang Spesifik, Bukan Semuanya
Menulis except: tanpa menyebut jenis exception memang praktis, tapi itu jebakan. except: dan except Exception: akan ikut menelan KeyboardInterrupt dan SystemExit — dua sinyal yang seharusnya membiarkan program berhenti saat user tekan Ctrl+C. Sebut exception yang benar-benar kamu targetkan.
def bagi(a: float, b: float) -> float:
try:
return a / b
except ZeroDivisionError:
print("Pembagi tidak boleh nol")
return 0.0
except TypeError:
print("Kedua argumen harus angka")
raise
print(bagi(10, 0)) # Pembagi tidak boleh nol -> 0.0
print(bagi(10, "2")) # Kedua argumen harus angka -> raise
Perhatikan raise telanjang di blok except TypeError. Itu melempar ulang exception yang sama persis, tanpa mengubah traceback aslinya. Berguna saat kamu hanya ingin mencatat kejadian tapi tetap membiarkan error itu naik ke pemanggil.
else dan finally: Pisahkan Sukses dari Bersih-bersih
else dijalankan hanya kalau blok try selesai tanpa error. finally selalu dijalankan, error atau tidak — tempat yang pas untuk menutup file, koneksi database, atau melepas lock.
def simpan(path: str, data: str) -> None:
f = open(path, "w")
try:
f.write(data)
except OSError as e:
print(f"Gagal menulis: {e}")
else:
print("Data berhasil disimpan")
finally:
f.close()
Untuk file dan resource lain, kamu sebaiknya pakai statement with daripada try/finally manual. Context manager menutup resource otomatis, bahkan saat ada exception. Saya bahas tuntas di Python Context Manager.
Bikin Exception Sendiri
Exception bawaan seperti ValueError dan KeyError sering cukup. Tapi begitu logika bisnis kamu punya aturan sendiri, exception khusus membuat kode jauh lebih mudah dibaca dan ditangani. Kamu cukup subclass Exception.
class SaldoTidakCukup(Exception):
def __init__(self, saldo: float, jumlah: float):
self.saldo = saldo
self.jumlah = jumlah
super().__init__(f"Saldo {saldo} tidak cukup untuk tarik {jumlah}")
class Rekening:
def __init__(self, saldo: float):
self.saldo = saldo
def tarik(self, jumlah: float) -> None:
if jumlah > self.saldo:
raise SaldoTidakCukup(self.saldo, jumlah)
self.saldo -= jumlah
rek = Rekening(100_000)
try:
rek.tarik(250_000)
except SaldoTidakCukup as e:
print(e) # Saldo 100000 tidak cukup untuk tarik 250000
Sekarang pemanggil bisa menangkap SaldoTidakCukup secara spesifik, dan pesan errornya menyimpan konteks yang dibutuhkan. Tidak perlu parsing string pesan yang rapuh.
raise ... from ...: Jaga Rantai Penyebab Error
Saat kamu menangkap exception lalu melempar yang baru, Python menyimpan exception asli di __cause__. Ini penting karena error di lapisan bawah sering jadi petunjuk utama. Kuncinya ada di sintaks from.
import requests
def ambil_harga_saham(ticker: str) -> float:
try:
response = requests.get(
f"https://api.example.com/harga/{ticker}", timeout=5
)
response.raise_for_status()
return response.json()["harga"]
except requests.RequestException as e:
raise RuntimeError(f"Gagal ambil harga {ticker}") from e
Traceback-nya akan menampilkan dua exception sekaligus, dengan baris The above exception was the direct cause of the following exception yang menuntun kamu ke akar masalah. Tanpa from e, penyebab aslinya tetap tersimpan tapi traceback-nya lebih berisik dan membingungkan.
Logging: Jangan Cuma Print
Di production, print() tidak cukup. Kamu butuh timestamp, level, dan traceback lengkap yang bisa dikirim ke file atau sistem monitoring. logging.exception() mencatat traceback secara otomatis.
import logging
logging.basicConfig(level=logging.ERROR)
try:
config = muat_konfigurasi("config.json")
except Exception:
logging.exception("Gagal memuat konfigurasi")
raise
Hasilnya berupa log lengkap dengan tipe exception, pesan, dan seluruh traceback. Kalau kamu belum terbiasa mengatur logger, format handler, dan rotasi file, baca Belajar Python Logging dulu — di sana saya kupas dari nol.
Exception di FastAPI
Kalau kamu membangun API dengan FastAPI, exception yang tidak tertangani akan berakhir sebagai respons 500 yang tidak informatif. FastAPI menyediakan HTTPException untuk mengubah error jadi respons dengan status code yang tepat.
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/rekening/{id_rekening}/tarik")
def tarik(id_rekening: int, jumlah: float):
rekening = ambil_rekening(id_rekening)
if rekening is None:
raise HTTPException(status_code=404, detail="Rekening tidak ditemukan")
try:
rekening.tarik(jumlah)
except SaldoTidakCukup as e:
raise HTTPException(status_code=400, detail=str(e))
return {"saldo": rekening.saldo}
Custom exception SaldoTidakCukup yang kita buat tadi kini jadi respons JSON 400 yang rapi, bukan crash. Kalau kamu baru mulai dengan framework ini, Cara Membuat REST API dengan FastAPI adalah titik awal yang pas.
Kesalahan yang Sering Saya Temukan
Yang paling umum: except Exception: pass. Kode jalan tapi error ditelan diam-diam, dan dua bulan kemudian kamu bingung kenapa data hilang tanpa jejak. Kalau kamu memang harus menelan exception, setidaknya log dulu sebelum pass.
Jangan pernah menangkap Exception lalu membiarkan program terus jalan seolah tidak ada yang terjadi, kecuali kamu punya alasan kuat dan sudah mencatatnya ke log. Bug yang diam lebih mahal daripada crash yang berisik.
Kesalahan kedua: menulis pesan error yang tidak berguna, seperti "Terjadi kesalahan" tanpa konteks. Sertakan input yang memicu error itu — nilai variabel, id user, nama file. Pesan yang bagus membuat debugging berikutnya butuh menit, bukan jam. Kalau kamu juga menulis test, pastikan behavior exception-nya diuji, bukan cuma happy path-nya.
Mulai dari satu fungsi yang sering gagal di proyek kamu. Bungkus dengan try/except yang spesifik, log errornya, lalu ubah satu-dua error jadi custom exception. Rasakan sendiri bedanya saat kamu harus debug kode itu sebulan kemudian.