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()
Klausa else pada try sering diabaikan tapi sangat berguna: kode di else hanya berjalan jika tidak ada eksepsi di try. Ini memisahkan kode “jalur sukses” dari kode “penanganan error”, membuat niat lebih jelas dibanding meletakkan semuanya di dalam try.

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: pass adalah 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) bukan Exception atau bare except. 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.
  • else untuk kode jalur sukses — kode di else hanya berjalan jika tidak ada eksepsi di try, memisahkan logika sukses dari logika error.
  • finally untuk cleanup resource — tutup file, koneksi, dan resource lainnya di finally agar selalu dieksekusi meski terjadi eksepsi.
  • raise from untuk exception chaining — mengubah eksepsi level rendah ke eksepsi domain sambil mempertahankan konteks asli untuk debugging.
  • Definisikan hierarki eksepsi kustomAppError sebagai base, lalu ValidationError, 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.suppress sebagai alternatif bersih untuk try/except: pass ketika memang ingin mengabaikan eksepsi tertentu.
  • ExceptionGroup dan except* (Python 3.11+) untuk skenario di mana beberapa eksepsi terjadi bersamaan — relevan untuk validasi batch dan kode async.

← Sebelumnya: Interface   Berikutnya: List →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact