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.

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

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

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

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

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

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

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