Eksepsi #
Eksepsi adalah cara Python memberi sinyal bahwa sesuatu yang tidak terduga terjadi saat program berjalan — file tidak ditemukan, koneksi terputus, input tidak valid, atau operasi matematika tidak mungkin dilakukan. Mekanisme try/except memungkinkan kamu menangkap sinyal ini dan meresponsnya dengan tepat, alih-alih membiarkan program crash. Yang sama pentingnya adalah memahami kapan tidak menangkap eksepsi — menangkap terlalu lebar atau terlalu diam adalah anti-pattern berbahaya yang menyembunyikan bug. Artikel ini membahas penanganan eksepsi Python secara komprehensif: dari hirarki built-in, pola try/except/else/finally, eksepsi kustom, hingga exception chaining dan ExceptionGroup di Python 3.11+.
Hirarki Eksepsi Built-in #
Semua eksepsi di Python mewarisi dari BaseException. Mengetahui hirarki ini penting agar kamu menangkap tipe yang tepat — tidak terlalu lebar, tidak terlalu sempit.
BaseException
├── SystemExit ← sys.exit() — jangan ditangkap kecuali perlu
├── KeyboardInterrupt ← Ctrl+C — jangan ditangkap kecuali perlu
├── GeneratorExit ← generator/coroutine ditutup
└── Exception ← induk semua eksepsi "normal"
├── ArithmeticError
│ ├── ZeroDivisionError
│ ├── OverflowError
│ └── FloatingPointError
├── AttributeError ← akses atribut yang tidak ada
├── ImportError
│ └── ModuleNotFoundError
├── LookupError
│ ├── IndexError ← indeks list di luar jangkauan
│ └── KeyError ← kunci dict tidak ditemukan
├── NameError
│ └── UnboundLocalError
├── OSError (IOError)
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── TimeoutError
├── RuntimeError
│ └── RecursionError
├── StopIteration
├── TypeError ← tipe tidak sesuai
├── ValueError ← nilai tidak valid untuk tipe tersebut
└── ...
# Menangkap kelas induk juga menangkap semua subkelasnya
try:
angka = int("bukan angka")
except ValueError:
print("ValueError ditangkap") # ← tertangkap
try:
d = {"a": 1}
print(d["b"])
except LookupError:
print("LookupError ditangkap") # ← KeyError adalah subkelas LookupError
try / except / else / finally
#
Blok penanganan eksepsi Python memiliki empat klausa yang bekerja bersama.
Untuk memvisualisasikan bagaimana alur kontrol mengalir melalui klausa-klausa ini — baik ketika eksepsi terjadi (dan tertangkap atau tidak tertangkap) maupun saat program berjalan sukses — perhatikan diagram siklus hidup eksepsi di bawah ini:
flowchart TD
Start["Masuk Blok try"] --> RunTry["Jalankan Kode di dalam try"]
RunTry --> CheckError{"Terjadi Eksepsi?"}
CheckError -->|Tidak| RunElse["Jalankan Blok else"]
RunElse --> RunFinally["Jalankan Blok finally"]
CheckError -->|Ya| CheckCatch{"Apakah Eksepsi Tertangkap oleh except?"}
CheckCatch -->|Ya| RunExcept["Jalankan Blok except"]
RunExcept --> RunFinally
CheckCatch -->|Tidak| RunFinallyUnhandled["Jalankan Blok finally"]
RunFinallyUnhandled --> Propagate["Rambatkan Eksepsi ke Call Stack Atas (Crash/Uncaught)"]
RunFinally --> End["Lanjutkan Program di Bawah try-except"]
Diagram di atas memperjelas bahwa blok finally dijamin akan selalu dieksekusi sebelum kontrol program meninggalkan struktur penanganan eksepsi, bahkan jika eksepsi yang terjadi tidak tertangkap oleh blok except manapun dan dirambatkan naik (propagated) ke tumpukan panggilan (call stack) di atasnya.
Berikut adalah contoh penggunaan lengkap keempat klausa tersebut:
try:
# Kode yang mungkin melempar eksepsi
hasil = 10 / 2
except ZeroDivisionError:
# Dijalankan HANYA jika ZeroDivisionError terjadi
print("Pembagian dengan nol!")
except (TypeError, ValueError) as e:
# Menangkap beberapa tipe sekaligus
print(f"Error tipe atau nilai: {e}")
else:
# Dijalankan HANYA jika TIDAK ada eksepsi di blok try
print(f"Berhasil: {hasil}")
finally:
# Selalu dijalankan — baik ada eksepsi maupun tidak
print("Selesai")
Peran Masing-masing Klausa #
import os
def baca_file_config(path: str) -> dict:
"""Membaca file konfigurasi dan mengembalikan dict."""
file = None
try:
file = open(path, "r", encoding="utf-8") # mungkin FileNotFoundError
konten = file.read()
return parse_config(konten) # mungkin ValueError
except FileNotFoundError:
print(f"File tidak ditemukan: {path}")
return {}
except ValueError as e:
print(f"Format konfigurasi tidak valid: {e}")
return {}
else:
# Hanya dieksekusi jika try selesai tanpa eksepsi
# Berguna untuk logging sukses
print(f"Konfigurasi berhasil dimuat dari {path}")
finally:
# Cleanup — selalu tutup file meski terjadi eksepsi
if file is not None:
file.close()
Klausaelsepadatrysering diabaikan tapi sangat berguna: kode dielsehanya berjalan jika tidak ada eksepsi ditry. Ini memisahkan kode “jalur sukses” dari kode “penanganan error”, membuat niat lebih jelas dibanding meletakkan semuanya di dalamtry.
Anti-Pattern Penanganan Eksepsi #
Ini adalah bagian terpenting yang jarang dibahas dengan cukup serius.
# ANTI-PATTERN 1: menangkap Exception terlalu lebar (bare except)
try:
hasil = proses_data(input_data)
except: # ← menangkap SEMUA termasuk SystemExit, KeyboardInterrupt
print("Terjadi error") # ← menyembunyikan bug, sulit di-debug
# ANTI-PATTERN 2: menangkap Exception dan diam (silent swallow)
try:
koneksi = buat_koneksi_db()
except Exception:
pass # ← bug tersembunyi sepenuhnya!
# ANTI-PATTERN 3: menangkap terlalu lebar dan hanya log
try:
kirim_email(pengguna)
except Exception as e:
print(e) # ← tetap terlalu lebar, semua error diperlakukan sama
# BENAR: tangkap tipe spesifik yang kamu harapkan
try:
koneksi = buat_koneksi_db()
except ConnectionRefusedError:
logger.error("Database tidak dapat dihubungi")
raise # ← re-raise agar pemanggil tahu ada masalah
except TimeoutError:
logger.warning("Koneksi timeout, mencoba ulang...")
koneksi = buat_koneksi_db(timeout=60)
# BENAR: jika memang perlu menangkap lebar, minimal log dengan traceback
import logging
try:
proses_batch(data)
except Exception:
logging.exception("Proses batch gagal") # ← logging.exception menyertakan traceback
raise # ← re-raise setelah logging
except Exception: passadalah salah satu anti-pattern paling berbahaya dalam Python. Ia menelan semua error — termasuk bug logika,MemoryError, bahkan kesalahan pemrograman — tanpa jejak apapun. Program terus berjalan seolah-olah tidak ada masalah, sementara state internal sudah rusak.
raise — Melempar Eksepsi
#
raise digunakan untuk melempar eksepsi secara eksplisit, baik eksepsi baru maupun meneruskan eksepsi yang sudah ditangkap.
# Melempar eksepsi baru
def bagi(a: float, b: float) -> float:
if b == 0:
raise ZeroDivisionError("Pembagi tidak boleh nol")
return a / b
# Melempar dengan pesan yang informatif
def simpan_pengguna(data: dict) -> None:
if "email" not in data:
raise ValueError("Field 'email' wajib ada dalam data pengguna")
if not isinstance(data.get("umur"), int):
raise TypeError(f"'umur' harus integer, bukan {type(data.get('umur')).__name__}")
# simpan ke database...
# Re-raise eksepsi yang sedang ditangkap (tanpa argumen)
def muat_konfigurasi(path: str) -> dict:
try:
return json.load(open(path))
except json.JSONDecodeError:
logging.error(f"Format JSON tidak valid: {path}")
raise # ← teruskan eksepsi yang sama ke pemanggil
Exception Chaining dengan raise from
#
raise X from Y membuat rantai eksepsi — eksepsi baru dilampirkan konteks dari eksepsi asli. Ini sangat berguna saat kamu mengubah eksepsi level rendah menjadi eksepsi level domain:
class DatabaseError(Exception):
"""Eksepsi domain untuk semua error database."""
pass
def ambil_pengguna(user_id: int) -> dict:
try:
return db.query(f"SELECT * FROM users WHERE id = {user_id}")
except ConnectionError as e:
# Ubah ke eksepsi domain, tapi pertahankan konteks asli
raise DatabaseError(f"Gagal mengambil pengguna {user_id}") from e
# Saat error terjadi, Python menampilkan KEDUA eksepsi:
# ConnectionError: ...
# The above exception was the direct cause of the following exception:
# DatabaseError: Gagal mengambil pengguna 42
# raise ... from None — sembunyikan eksepsi asli secara eksplisit
def parse_tanggal(teks: str):
try:
return datetime.strptime(teks, "%Y-%m-%d")
except ValueError:
# Kita tidak ingin tampilkan detail ValueError internal
raise ValueError(f"Format tanggal tidak valid: '{teks}' (gunakan YYYY-MM-DD)") from None
Eksepsi Kustom #
Mendefinisikan kelas eksepsi sendiri membuat error lebih deskriptif, lebih mudah ditangkap secara spesifik, dan bisa membawa informasi tambahan yang relevan.
Hierarki Eksepsi Kustom #
# Definisikan hierarki eksepsi untuk domain aplikasi kamu
class AppError(Exception):
"""Kelas dasar untuk semua eksepsi aplikasi ini."""
pass
class ValidationError(AppError):
"""Input pengguna tidak memenuhi syarat validasi."""
pass
class AuthenticationError(AppError):
"""Kredensial tidak valid atau sesi kadaluarsa."""
pass
class AuthorizationError(AppError):
"""Pengguna tidak memiliki izin untuk aksi ini."""
pass
class NotFoundError(AppError):
"""Resource yang diminta tidak ditemukan."""
def __init__(self, resource: str, identifier):
self.resource = resource
self.identifier = identifier
super().__init__(f"{resource} dengan id '{identifier}' tidak ditemukan")
class RateLimitError(AppError):
"""Terlalu banyak permintaan dalam rentang waktu tertentu."""
def __init__(self, batas: int, reset_detik: int):
self.batas = batas
self.reset_detik = reset_detik
super().__init__(
f"Batas {batas} permintaan tercapai. "
f"Coba lagi dalam {reset_detik} detik."
)
Eksepsi dengan Atribut Tambahan #
from dataclasses import dataclass
from typing import Any
class ValidationError(AppError):
"""Eksepsi validasi dengan detail field yang bermasalah."""
def __init__(self, field: str, nilai: Any, pesan: str):
self.field = field
self.nilai = nilai
self.pesan = pesan
super().__init__(f"Validasi gagal pada field '{field}': {pesan}")
def __str__(self) -> str:
return (
f"ValidationError(\n"
f" field='{self.field}',\n"
f" nilai={self.nilai!r},\n"
f" pesan='{self.pesan}'\n"
f")"
)
# Penggunaan
def validasi_email(email: str) -> str:
if not email:
raise ValidationError("email", email, "Tidak boleh kosong")
if "@" not in email:
raise ValidationError("email", email, "Format tidak valid — harus mengandung '@'")
if len(email) > 254:
raise ValidationError("email", email, f"Terlalu panjang ({len(email)} karakter, maks 254)")
return email.lower().strip()
try:
validasi_email("bukan-email")
except ValidationError as e:
print(e.field) # → email
print(e.nilai) # → bukan-email
print(e.pesan) # → Format tidak valid — harus mengandung '@'
print(e) # → ValidationError(...)
Menangkap Berdasarkan Hierarki #
def proses_permintaan(user_id: int, aksi: str) -> None:
try:
pengguna = ambil_pengguna(user_id) # bisa NotFoundError
verifikasi_izin(pengguna, aksi) # bisa AuthorizationError
jalankan_aksi(pengguna, aksi) # bisa ValidationError
except NotFoundError as e:
return {"status": 404, "pesan": str(e)}
except AuthorizationError:
return {"status": 403, "pesan": "Akses ditolak"}
except ValidationError as e:
return {"status": 400, "pesan": str(e), "field": e.field}
except AppError as e:
# Tangkap semua error aplikasi yang tidak tertangani di atas
logging.error(f"AppError tidak terduga: {e}")
return {"status": 500, "pesan": "Terjadi kesalahan internal"}
contextlib.suppress — Abaikan Eksepsi Tertentu
#
Jika memang ada kasus di mana kamu ingin mengabaikan eksepsi tertentu tanpa blok try/except yang verbose:
from contextlib import suppress
# ANTI-PATTERN: try/except hanya untuk mengabaikan
try:
os.remove("file_sementara.tmp")
except FileNotFoundError:
pass # tidak apa-apa jika file memang tidak ada
# BENAR: suppress lebih ekspresif untuk kasus ini
with suppress(FileNotFoundError):
os.remove("file_sementara.tmp")
# Bisa suppress beberapa tipe sekaligus
with suppress(FileNotFoundError, PermissionError):
os.remove("file_sementara.tmp")
ExceptionGroup — Beberapa Eksepsi Sekaligus (Python 3.11+)
#
Python 3.11 memperkenalkan ExceptionGroup untuk menangani situasi di mana beberapa eksepsi terjadi bersamaan — sangat relevan dalam konteks async/concurrent:
# Melempar ExceptionGroup
def validasi_form(data: dict) -> None:
errors = []
if not data.get("nama"):
errors.append(ValidationError("nama", data.get("nama"), "Wajib diisi"))
if not data.get("email"):
errors.append(ValidationError("email", data.get("email"), "Wajib diisi"))
if data.get("umur", 0) < 18:
errors.append(ValidationError("umur", data.get("umur"), "Harus minimal 18"))
if errors:
raise ExceptionGroup("Validasi form gagal", errors)
# Menangkap dengan except* (sintaks baru Python 3.11+)
try:
validasi_form({"nama": "", "email": "", "umur": 15})
except* ValidationError as eg:
print(f"Ada {len(eg.exceptions)} error validasi:")
for err in eg.exceptions:
print(f" - {err.field}: {err.pesan}")
# → Ada 3 error validasi:
# → - nama: Wajib diisi
# → - email: Wajib diisi
# → - umur: Harus minimal 18
Praktik Terbaik Penanganan Eksepsi #
import logging
# 1. Tangkap setepat mungkin
try:
data = json.loads(teks)
except json.JSONDecodeError as e: # ← spesifik, bukan Exception
logging.warning(f"JSON tidak valid pada baris {e.lineno}: {e.msg}")
data = {}
# 2. Selalu sertakan konteks dalam pesan error
def muat_produk(produk_id: int):
try:
return db.get("produk", produk_id)
except KeyError:
# ANTI-PATTERN: pesan tidak informatif
# raise ValueError("Produk tidak ditemukan")
# BENAR: sertakan informasi yang membantu debugging
raise NotFoundError("Produk", produk_id)
# 3. Gunakan finally untuk cleanup resource
def proses_dengan_koneksi():
koneksi = None
try:
koneksi = buat_koneksi()
return koneksi.query("SELECT ...")
except DatabaseError as e:
logging.error(f"Query gagal: {e}")
raise
finally:
if koneksi:
koneksi.close() # ← selalu tutup, meski terjadi eksepsi
# 4. Lebih baik: gunakan context manager
from contextlib import contextmanager
@contextmanager
def koneksi_db():
koneksi = buat_koneksi()
try:
yield koneksi
finally:
koneksi.close()
# Penggunaan yang bersih
with koneksi_db() as db:
hasil = db.query("SELECT ...")
Ringkasan #
- Tangkap setepat mungkin — gunakan tipe eksepsi yang spesifik (
FileNotFoundError,ValueError) bukanExceptionatau bareexcept. Semakin spesifik, semakin mudah debug.- Jangan pernah
except Exception: pass— ini menelan semua error termasuk bug, membuat program berjalan dengan state rusak tanpa jejak apapun.elseuntuk kode jalur sukses — kode dielsehanya berjalan jika tidak ada eksepsi ditry, memisahkan logika sukses dari logika error.finallyuntuk cleanup resource — tutup file, koneksi, dan resource lainnya difinallyagar selalu dieksekusi meski terjadi eksepsi.raise fromuntuk exception chaining — mengubah eksepsi level rendah ke eksepsi domain sambil mempertahankan konteks asli untuk debugging.- Definisikan hierarki eksepsi kustom —
AppErrorsebagai base, laluValidationError,NotFoundError, dll. sebagai subkelas. Memungkinkan penangkap yang fleksibel dari luas hingga spesifik.- Beri eksepsi kustom atribut tambahan — menyimpan
field,kode, atau data konteks lain di dalam eksepsi membantu penangkap mengambil tindakan yang tepat.contextlib.suppresssebagai alternatif bersih untuktry/except: passketika memang ingin mengabaikan eksepsi tertentu.ExceptionGroupdanexcept*(Python 3.11+) untuk skenario di mana beberapa eksepsi terjadi bersamaan — relevan untuk validasi batch dan kode async.