2Python untuk AI

Error Handling & Debugging

4 jam14 min baca
Tujuan

Handle exception dengan elegan, dan debug masalah dengan sistematis. Skill yang dipakai sepanjang karir.

08 — Error Handling & Debugging

Estimasi: 4 jam Tujuan: Handle exception dengan elegan, dan debug masalah dengan sistematis. Skill yang dipakai sepanjang karir.


Kenapa Materi Ini Penting?

ML practitioner habiskan lebih banyak waktu untuk debug daripada nulis kode baru. Model crash karena tensor shape mismatch, training stop karena GPU OOM, prediction salah karena edge case di data. Tanpa skill error handling yang matang, kamu akan stuck berjam-jam di error yang sebenarnya bisa di-handle dalam 5 menit.

Lebih dari sekadar "bungkus dengan try/except", error handling profesional itu soal mendesain failure mode: kapan retry, kapan gracefully degrade, kapan log dan alert, kapan crash loud dan jelas. Production ML system tanpa error handling proper = bom waktu.

Analogi besar: error handling = safety net di sirkus. Kamu tetap mau atraksi yang berani (kode yang ambisius), tapi siapkan jaring kalau jatuh. Tanpa jaring, satu fall = game over. Dengan jaring, kamu bisa coba lagi.


Peta Konsep

flowchart TD
    A[⚠️ Error Handling] --> B[🚨 Exception]
    A --> C[🛡️ try/except]
    A --> D[🔄 raise]
    A --> E[🎯 Custom Exception]
    A --> F[🔍 Debugging]

    C --> C1[try]
    C --> C2[except]
    C --> C3[else]
    C --> C4[finally]

    F --> F1[Traceback]
    F --> F2[print debug]
    F --> F3[logging]
    F --> F4[pdb / debugger]

Diagram Alur try/except/else/finally

Cara Membaca Diagram

Flow kiri-ke-kanan dengan 2 jalur utama: sukses (atas, hijau) dan error (bawah, pink). Kedua jalur akhirnya converge ke finally. Node finally dijamin selalu dijalankan — itulah fungsinya untuk cleanup.

Walkthrough Step-by-Step

  1. Mulai — masuk try block.
  2. try block — eksekusi kode berisiko (parse input, bagi angka, akses API).
  3. Cek Error?
    • Tidak → ke else block (jalan kalau sukses).
    • Ya → ke check match except.
  4. Match except?
    • Ya → eksekusi handler (except ValueError: ...). Error ditangani.
    • Tidak → exception propagate naik ke caller.
  5. finally — selalu dijalankan, sukses maupun gagal. Cocok untuk cleanup (close file, release lock).
  6. End — kalau handler success, lanjut program. Kalau propagate, exception naik ke level atas.

Analogi Sehari-hari

Try/except = safety net di sirkus. try = atraksi berani (lompat antar gantungan). except = jaring bawah yang siap kalau jatuh. else = perayaan saat sukses landing. finally = bersih-bersih panggung — apapun hasilnya (sukses atau jatuh), panggung harus dibersihkan. Tanpa jaring, satu jatuh = game over. Dengan jaring, kamu bisa coba lagi.

Diagram statis Mermaid sebagai fallback:

flowchart TD
    Start([Mulai]) --> T[try block]
    T --> Q{Error?}
    Q -->|Tidak| EL[else block]
    Q -->|Ya| EX{except match?}
    EX -->|Ya| H[handle exception]
    EX -->|Tidak| RE[propagate exception]
    H --> F[finally block]
    EL --> F
    RE --> F
    F --> End([Lanjut / Re-raise])

Analogi:

  • try = "coba ini"
  • except = "kalau gagal, lakukan ini"
  • else = "kalau sukses, lakukan ini juga"
  • finally = "tidak peduli sukses/gagal, lakukan ini di akhir" (tutup file, release resource)

Bagian 1 — Apa Itu Exception?

Exception = error saat runtime yang menghentikan program.

print(10 / 0)
# ZeroDivisionError: division by zero
data = [1, 2, 3]
print(data[10])
# IndexError: list index out of range

Tanpa handling, program crash. Dengan handling, program bisa lanjut atau gracefully fail.

Built-in Exception Penting

Exception Kapan
ValueError Tipe benar, value salah (int("abc"))
TypeError Tipe salah ("a" + 1)
KeyError Key tidak ada di dict
IndexError Index di luar range
FileNotFoundError File tidak ada
ZeroDivisionError Bagi 0
AttributeError Method/attribute tidak ada
ImportError Gagal import
NameError Variabel tidak terdefinisi
RuntimeError Generic runtime error
Exception Base class semua

Bagian 2 — try/except

Bentuk Dasar

try:
    angka = int(input("Masukkan angka: "))
    print(f"Kuadrat: {angka ** 2}")
except ValueError:
    print("Input bukan angka!")

Kalau int() gagal → loncat ke except. Program tidak crash.

Multiple Except

try:
    angka = int(input("Angka: "))
    hasil = 100 / angka
except ValueError:
    print("Bukan angka!")
except ZeroDivisionError:
    print("Tidak boleh 0!")

Except Multiple Sekaligus

try:
    # ...
except (ValueError, TypeError) as e:
    print(f"Error: {e}")

Catch-all (Hindari!)

# ❌ Buruk — sembunyikan semua bug
try:
    do_something()
except:
    pass

# ✅ Lebih baik — minimal log
try:
    do_something()
except Exception as e:
    print(f"Error: {e}")
    raise   # re-raise kalau memang tidak bisa di-handle

Aturan: HINDARI bare except: dan except Exception: tanpa handling spesifik. Itu menyembunyikan bug.

else — Jalan Kalau Tidak Ada Error

try:
    angka = int(input("Angka: "))
except ValueError:
    print("Bukan angka!")
else:
    print(f"Kuadrat: {angka ** 2}")    # hanya jalan kalau try sukses

finally — Selalu Jalan

try:
    f = open("data.txt")
    # ...
except FileNotFoundError:
    print("File tidak ada")
finally:
    f.close()    # selalu jalan, walau ada error atau tidak

Modern alternative: pakai with statement untuk context manager. Lebih bersih.


Bagian 3 — Raise Exception

Bikin error sendiri:

def hitung_bmi(berat, tinggi):
    if berat <= 0:
        raise ValueError("Berat harus positif")
    if tinggi <= 0:
        raise ValueError("Tinggi harus positif")
    return berat / (tinggi ** 2)

hitung_bmi(-70, 1.7)
# ValueError: Berat harus positif

Re-raise

try:
    do_something()
except ValueError as e:
    log_error(e)
    raise         # re-raise yang sama

# Atau dengan context tambahan
try:
    do_something()
except ValueError as e:
    raise RuntimeError(f"Gagal di step X: {e}") from e

from e mempertahankan original exception trace — penting untuk debugging.


Bagian 4 — Custom Exception

Diagram: Hierarki Exception

classDiagram
    class Exception
    class BankError {
        +str message
    }
    class InsufficientBalanceError
    class InvalidAmountError
    class AccountFrozenError
    Exception <|-- BankError
    BankError <|-- InsufficientBalanceError
    BankError <|-- InvalidAmountError
    BankError <|-- AccountFrozenError

Analogi: custom exception = kategori error sesuai domain bisnis. Bank punya "saldo kurang", e-commerce punya "barang habis", auth punya "token expired". Kategori yang spesifik bikin handling lebih clean.

Bikin exception class sendiri untuk domain spesifik:

class InsufficientBalanceError(Exception):
    """Saldo tidak cukup."""
    pass

class InvalidAmountError(Exception):
    """Jumlah tidak valid."""
    pass

class BankAccount:
    def __init__(self, saldo=0):
        self.saldo = saldo
    
    def tarik(self, jumlah):
        if jumlah <= 0:
            raise InvalidAmountError(f"Jumlah harus positif, got {jumlah}")
        if jumlah > self.saldo:
            raise InsufficientBalanceError(
                f"Saldo {self.saldo} kurang dari {jumlah}"
            )
        self.saldo -= jumlah

# Pakai
try:
    acc = BankAccount(1000)
    acc.tarik(2000)
except InsufficientBalanceError as e:
    print(f"Saldo kurang: {e}")
except InvalidAmountError as e:
    print(f"Jumlah error: {e}")

Best practice: custom exception membuat error handling lebih clear. Tidak perlu untuk script kecil. Wajib untuk library.


Bagian 5 — EAFP vs LBYL (Pythonic Style)

Dua filosofi handling:

LBYL = Look Before You Leap (cek dulu sebelum aksi)

if "key" in d:
    value = d["key"]
else:
    value = default

EAFP = Easier to Ask Forgiveness than Permission (coba aja, handle kalau gagal)

try:
    value = d["key"]
except KeyError:
    value = default

Python lebih suka EAFP. Tapi banyak shortcut Pythonic:

# Lebih Pythonic
value = d.get("key", default)

# Untuk check file
from pathlib import Path
if Path("file.txt").exists():
    pass

Pilih yang lebih bersih per kasus. Yang penting konsisten dalam satu codebase.


Bagian 6 — Debugging

Saat error muncul, baca traceback dari bawah ke atas (kebanyakan kasus):

Traceback (most recent call last):
  File "main.py", line 10, in <module>
    process(data)
  File "main.py", line 6, in process
    return data["nama"]
KeyError: 'nama'
  • Bottom: error type + message (paling info)
  • Middle: function call yang error
  • Top: entry point

Klasik tapi efektif:

def hitung(a, b):
    print(f"DEBUG: a={a}, b={b}, type={type(a)}")
    return a + b

Pakai f-string {var=}:

x = 42
print(f"{x = }")    # "x = 42"

Logging (Lebih Profesional)

import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

logger.debug("Detail untuk dev")
logger.info("Info biasa")
logger.warning("Warning")
logger.error("Error")
logger.critical("Critical")

# Output dengan timestamp
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s'
)

Debugger Built-in (pdb)

import pdb

def buggy():
    x = 10
    y = 20
    pdb.set_trace()    # akan pause di sini
    return x + y

Saat program berhenti, ketik:

  • n next line
  • s step into function
  • c continue
  • p var print variable
  • q quit

Modern: VS Code Debugger

Setup launch config (.vscode/launch.json):

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Current File",
            "type": "python",
            "request": "launch",
            "program": "${file}",
            "console": "integratedTerminal"
        }
    ]
}

Set breakpoint (klik di gutter sebelah line number → titik merah). Run dengan F5. Jauh lebih powerful dari print debugging.


Bagian 7 — Assert (Defensive Programming)

def hitung_bmi(berat, tinggi):
    assert berat > 0, "Berat harus positif"
    assert tinggi > 0, "Tinggi harus positif"
    return berat / (tinggi ** 2)

hitung_bmi(-70, 1.7)
# AssertionError: Berat harus positif

Assert untuk invariants (sesuatu yang harus selalu benar).

Hati-hati: assert bisa di-disable dengan python -O. Jangan pakai untuk validation kritis (security, business logic). Pakai if + raise untuk itu.


Bagian 8 — Common Errors & Solutions

IndentationError

def foo():
print("hi")     # ❌ tidak indent

Solusi: pakai 4 spasi konsisten.

KeyError di Dict

data = {"a": 1}
print(data["b"])    # ❌ KeyError

# Solusi: gunakan .get()
print(data.get("b", default))

IndexError di List

data = [1, 2, 3]
print(data[10])    # ❌ IndexError

# Solusi: cek dulu, atau pakai try/except
if 10 < len(data):
    print(data[10])

TypeError: Concatenate

nama = "Budi"
umur = 25
print("Halo " + umur)    # ❌ TypeError (str + int)

# Solusi: f-string
print(f"Halo {nama}, umur {umur}")

AttributeError: NoneType

def cari_user(id):
    # ... return None kalau tidak ada
    return None

user = cari_user(99)
print(user.nama)    # ❌ AttributeError: NoneType has no attribute

# Solusi: cek None
if user is not None:
    print(user.nama)

ImportError

import some_lib    # ❌ ModuleNotFoundError

# Solusi: install
# pip install some_lib

# Atau pastikan virtual env aktif
# conda activate ai-prep

ValueError: Convert

int("abc")          # ❌ ValueError
int("3.14")         # ❌ ValueError (str → int harus integer murni)

# Solusi
try:
    n = int(value)
except ValueError:
    n = 0    # default

Encoding Error

open("data.txt").read()    # ❌ UnicodeDecodeError di Windows kadang

# Solusi: spesifikasi encoding
open("data.txt", encoding="utf-8").read()

Bagian 9 — Defensive Programming Patterns

Validate Early, Fail Fast

def process_user(data: dict):
    # Validasi di awal
    if not isinstance(data, dict):
        raise TypeError(f"Expected dict, got {type(data)}")
    
    required = {"nama", "email"}
    missing = required - data.keys()
    if missing:
        raise ValueError(f"Missing keys: {missing}")
    
    # Logic utama
    # ...

Type Hints + isinstance

def hitung(a: int, b: int) -> int:
    if not isinstance(a, int):
        raise TypeError(f"a harus int, got {type(a)}")
    return a + b

Default Pattern

# Hindari nested if untuk default
def get_name(user, default="Anonymous"):
    if user is None:
        return default
    if not user.get("nama"):
        return default
    return user["nama"]

# Lebih ringkas
def get_name(user, default="Anonymous"):
    return (user or {}).get("nama") or default

Context Manager Sendiri

class Timer:
    def __enter__(self):
        import time
        self.start = time.time()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        elapsed = time.time() - self.start
        print(f"Elapsed: {elapsed:.2f}s")

with Timer():
    # ... slow code
    pass

Common Mistakes & FAQ

❌ Mistake 1: Bare except:

# ❌ tangkap semua, termasuk KeyboardInterrupt!
try:
    do_something()
except:
    pass    # silently swallow

# ✅ minimal pakai Exception, dan log
try:
    do_something()
except Exception as e:
    logger.error(f"Failed: {e}")
    raise

❌ Mistake 2: Pesan error tidak informatif

# ❌
raise ValueError("Error")

# ✅
raise ValueError(f"Berat harus positif, got {berat}")

❌ Mistake 3: Pakai assert untuk validasi production

# ❌ assert bisa di-disable dengan python -O
def transfer(amount):
    assert amount > 0    # bisa skip!
    
# ✅ pakai if + raise untuk kondisi penting
def transfer(amount):
    if amount <= 0:
        raise ValueError("Jumlah harus positif")

❌ Mistake 4: Catch terlalu luas

# ❌ menyembunyikan TypeError yang sebenarnya bug code
try:
    user_data = fetch_user(id)
    name = user_data.name
except Exception:
    name = "Unknown"

# ✅ catch yang spesifik
try:
    user_data = fetch_user(id)
    name = user_data.name
except UserNotFoundError:
    name = "Unknown"

❌ Mistake 5: Re-raise tanpa context

try:
    process_data(d)
except KeyError as e:
    raise RuntimeError("Failed")    # ❌ context hilang

# ✅ pakai from e
try:
    process_data(d)
except KeyError as e:
    raise RuntimeError(f"Missing key while processing: {e}") from e

❌ Mistake 6: try block terlalu besar

# ❌ tidak jelas baris mana yang error
try:
    data = load_csv(path)
    cleaned = clean_data(data)
    model = train(cleaned)
    save(model)
except Exception as e:
    print(e)

# ✅ sempit, exception spesifik
try:
    data = load_csv(path)
except FileNotFoundError:
    print(f"File tidak ada: {path}")
    return
# proses lain di luar try kalau aman

FAQ

Q: Kapan pakai if-check (LBYL) vs try-except (EAFP)? A:

  • LBYL = kalau cek murah dan harus dilakukan (e.g. cek argument valid)
  • EAFP = kalau race condition mungkin (e.g. file mungkin terhapus antara cek dan akses), atau cek mahal

Python lebih suka EAFP, tapi dict.get(k, default) adalah LBYL Pythonic.

Q: Apakah exception lambat? A: Raise/catch ada overhead, tapi tidak signifikan untuk kasus biasa. Jangan pakai exception sebagai control flow biasa (jangan bikin loop bergantung exception).

Q: Bagaimana cara test exception? A: Pakai pytest:

import pytest

def test_negative_balance():
    with pytest.raises(ValueError, match="positif"):
        hitung_bmi(-70, 1.7)

Q: Logging vs print untuk debug? A: Untuk script kecil, print boleh. Untuk production: logging — bisa diatur level (DEBUG/INFO/WARNING), output ke file, format dengan timestamp, disable di production tanpa edit kode.

Q: Bagaimana baca traceback yang panjang (deep stack)? A:

  1. Mulai dari bawah (error message)
  2. Naik ke frame milik kamu (file kamu, bukan stdlib)
  3. Cek apa state variable di titik error

Q: Bagaimana set breakpoint di Python tanpa IDE? A:

breakpoint()    # Python 3.7+

Akan masuk ke pdb mode. Atau pakai VS Code debugger (lebih recommend).


Cek Pemahaman

  • Bisa pakai try/except untuk handle error?
  • Tahu kapan pakai except spesifik vs catch-all?
  • Bisa raise exception sendiri?
  • Tahu beda assert dan raise?
  • Bisa baca traceback dari bawah ke atas?
  • Tahu beda EAFP dan LBYL?
  • Bisa setup logging?

Challenge 2.8

Challenge 1 — Safe Calculator

Modifikasi calculator dari challenge sebelumnya:

  • Handle ValueError saat input bukan angka
  • Handle ZeroDivisionError
  • Loop terus sampai user ketik exit
  • Log semua error ke file errors.log

Challenge 2 — Custom Exception

Bikin sistem perpustakaan:

  • BookNotFoundError, MemberNotFoundError, BookAlreadyBorrowedError
  • Method pinjam/kembali yang raise exception sesuai

Challenge 3 — File Reader yang Robust

Function safe_read_json(path):

  • Return dict kalau sukses
  • Return None kalau file tidak ada
  • Print warning kalau JSON malformed
  • Handle encoding error

Challenge 4 — Validator

Bikin validate_user(data: dict):

  • Wajib: nama (string ≥ 2 huruf), email (regex valid)
  • Optional: umur (int positif)
  • Raise ValueError dengan pesan jelas

Test dengan beberapa input invalid.

Challenge 5 — Retry Logic

Function retry(func, max_attempts=3):

  • Coba panggil func
  • Kalau error, retry sampai max_attempts
  • Kalau masih gagal, raise
def random_fail():
    import random
    if random.random() < 0.7:
        raise RuntimeError("Random failure")
    return "Success"

result = retry(random_fail, max_attempts=5)

Challenge 6 — Logging Decorator (Sneak Peek Dari Fase 9)

Bikin decorator @log_calls yang log setiap kali function dipanggil:

  • Print: nama function, args, kwargs, hasil
  • Log error kalau exception
@log_calls
def add(a, b):
    return a + b

add(2, 3)
# LOG: add called with (2, 3) = 5

(Kalau bingung, lewati dulu, kerjain di file 09.)

Challenge 7 — Debug Yourself

Run kode ini, baca traceback, dan fix:

def process(data):
    return data['nama'].upper()

users = [
    {'nama': 'Budi'},
    {'name': 'Ani'},        # bug 1
    {'nama': None},          # bug 2
    'Cici',                  # bug 3
]

for u in users:
    print(process(u))

Tulis di jurnal: bug apa, traceback bilang apa, fix-nya gimana.


Selanjutnya: 09-advanced.md — decorator, generator, context manager, type hints lanjutan. Yang membuat code kamu jadi level pro.