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