Sepanjang section ini kita telah memakai berbagai jenis exception dan error secara terpisah: RuntimeException, InvalidArgumentException, TypeError, hingga DivisionByZeroError. Semua tipe tersebut sebenarnya adalah anggota satu pohon keluarga besar yang berakar pada interface Throwable. Memahami pohon ini penting karena blok catch bekerja seperti instanceof: menangkap sebuah tipe berarti menangkap semua turunannya sekaligus. Pada chapter ini kita memetakan pohon tersebut secara menyeluruh dan melatih cara memanfaatkannya dalam urutan blok catch.

Pohon Keluarga Throwable #

Interface Throwable adalah akar dari seluruh objek yang bisa dilempar di PHP. Interface ini hanya memiliki dua cabang implementasi langsung: class Error dan class Exception. Dari kedua cabang itulah seluruh jenis kegagalan yang kita kenal diturunkan, mulai dari TypeError hingga PDOException. Diagram berikut merangkum bagian terpentingnya:

Diagram di atas menegaskan dua hal. Pertama, Error dan Exception adalah dua dunia yang terpisah: tidak ada TypeError yang menjadi turunan Exception, dan tidak ada RuntimeException yang menjadi turunan Error. Kedua, kedua cabang berbagi satu kakek yang sama, sehingga Throwable bisa dipakai untuk menangkap keduanya sekaligus. Seluruh method yang sudah kita pakai (getMessage(), getCode(), getPrevious()) dideklarasikan di interface ini, itulah sebabnya seluruh turunannya memiliki perilaku seragam.

Cabang Error untuk Kegagalan Mesin #

Cabang Error dibawa oleh mesin PHP sendiri untuk kondisi yang menandakan kode rusak, bukan sekadar keadaan bisnis yang tidak menguntungkan. Argumen bertipe salah, nilai di luar batas, dan sintaks yang tidak valid adalah contoh kategori tersebut. Anggota yang paling sering dijumpai:

ClassPemicu
TypeErrorTipe argumen atau nilai kembali tidak cocok dengan deklarasi
ArgumentCountErrorJumlah argumen pemanggilan kurang (turunan TypeError)
ValueErrorNilai argumen di luar rentang yang diterima function
ArithmeticErrorOperasi aritmetika yang tidak dapat diselesaikan
DivisionByZeroErrorPembagian atau modulo dengan nol (turunan ArithmeticError)
ParseErrorSintaks tidak valid saat kompilasi, misalnya dari eval()
UnhandledMatchErrorEkspresi match tanpa lengan yang cocok dan tanpa default

Mari kita lihat tiga anggota paling umum dalam aksi. Contoh pertama menangkap TypeError yang muncul karena argumen string diberikan kepada parameter bertipe int:

php
<?php
declare(strict_types=1);

function kaliDua(int $angka): int
{
    return $angka * 2;
}

try {
    echo kaliDua(21) . "\n";
    echo kaliDua("tujuh");
} catch (TypeError $e) {
    $pesan = strstr($e->getMessage(), ", called in", true) ?? $e->getMessage();
    echo "Tertangkap TypeError: $pesan\n";
}

Output yang Dihasilkan:

text
42
Tertangkap TypeError: kaliDua(): Argument #1 ($angka) must be of type int, string given

Pemanggilan pertama berjalan normal dan mencetak 42. Pemanggilan kedua melempar TypeError karena string "tujuh" tidak bisa dipakai sebagai int. Baris di dalam blok catch sengaja memangkas pesan sebelum frasa , called in menggunakan strstr(), sebab bagian akhir pesan TypeError memuat lokasi berkas dan nomor baris yang panjang dan berubah-ubah antar lingkungan.

Contoh kedua memperlihatkan DivisionByZeroError, anak dari ArithmeticError, yang sengaja ditangkap oleh blok catch induknya:

php
<?php
function bagiRata(int $total, int $pembagi): int
{
    return intdiv($total, $pembagi);
}

try {
    echo bagiRata(100, 4) . "\n";
    echo bagiRata(100, 0) . "\n";
} catch (ArithmeticError $e) {
    echo "Tertangkap ArithmeticError: " . get_class($e) . " - " . $e->getMessage() . "\n";
}

Output yang Dihasilkan:

text
25
Tertangkap ArithmeticError: DivisionByZeroError - Division by zero

Karena DivisionByZeroError adalah turunan ArithmeticError, blok catch (ArithmeticError $e) menangkap kegagalan pembagian tersebut. Method get_class() membuktikan bahwa objek yang tertangkap tetap bertipe DivisionByZeroError walau ditangkap oleh tipe induknya. Contoh ketiga memadukan ValueError dan ParseError:

php
<?php
try {
    echo str_repeat("ab", 3) . "\n";
    str_repeat("ab", -1);
} catch (ValueError $e) {
    echo "Tertangkap ValueError: " . $e->getMessage() . "\n";
}

try {
    eval('echo "Halo tanpa penutup');
} catch (ParseError $e) {
    echo "Tertangkap ParseError: " . $e->getMessage() . "\n";
}

Output yang Dihasilkan:

text
ababab
Tertangkap ValueError: str_repeat(): Argument #2 ($times) must be greater than or equal to 0
Tertangkap ParseError: syntax error, unexpected end of file, expecting variable or "${" or "{$"

Function str_repeat() menolak jumlah pengulangan negatif dengan melempar ValueError, sedangkan eval() dengan string yang sintaksnya tidak lengkap menghasilkan ParseError yang bisa ditangkap. Perlu dicatat bahwa ParseError dari berkas utama (bukan dari eval()) terjadi sebelum program berjalan, sehingga tidak mungkin ditangkap oleh try-catch di dalam berkas yang sama.

Cabang Exception dan Keluarga SPL #

Cabang Exception adalah rumah bagi kegagalan yang dilempar secara eksplisit oleh kode, baik dari SPL, ekstensi, maupun kode kita sendiri. SPL membagi keluarganya menjadi dua filosofi: LogicException untuk kesalahan yang seharusnya terdeteksi dan diperbaiki oleh penulis kode, serta RuntimeException untuk kegagalan yang wajar terjadi saat program berjalan. Tabel berikut memetakan seluruh anggota SPL:

ClassIndukKegunaan
LogicExceptionExceptionInduk keluarga kesalahan logika program
BadFunctionCallExceptionLogicExceptionFunction dipanggil dengan cara yang tidak benar
BadMethodCallExceptionLogicExceptionMethod dipanggil dengan cara yang tidak benar
DomainExceptionLogicExceptionNilai melanggar aturan domain bisnis
InvalidArgumentExceptionLogicExceptionArgumen tidak sesuai kontrak function
LengthExceptionLogicExceptionPanjang ilegal, misalnya string melebihi batas
OutOfRangeExceptionLogicExceptionPermintaan nilai di luar jangkauan yang valid
RuntimeExceptionExceptionInduk keluarga kegagalan saat eksekusi
OutOfBoundsExceptionRuntimeExceptionNilai kunci ilegal saat runtime
OverflowExceptionRuntimeExceptionMenambah elemen ke wadah yang sudah penuh
RangeExceptionRuntimeExceptionNilai di luar rentang saat runtime
UnderflowExceptionRuntimeExceptionOperasi pada wadah yang kosong
UnexpectedValueExceptionRuntimeExceptionNilai kembalian tidak sesuai dugaan

Di luar SPL, ekstensi PHP juga menambahkan anggota sendiri ke cabang Exception: PDOException menempel di bawah RuntimeException, sementara JsonException dari ekstensi JSON menjadi anak langsung Exception. Contoh berikut menangkap JsonException yang muncul ketika json_decode() dipanggil dengan flag JSON_THROW_ON_ERROR:

php
<?php
try {
    json_decode("{ json rusak", flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
    echo "Tertangkap JsonException: " . $e->getMessage() . "\n";
}

Output yang Dihasilkan:

text
Tertangkap JsonException: Syntax error

Tanpa flag JSON_THROW_ON_ERROR, function json_decode() hanya mengembalikan null dan menyimpan kode galatnya di json_last_error(). Dengan flag tersebut, kegagalan parsing berubah menjadi JsonException bertipe Exception, sehingga bisa ditangani dengan pola try-catch yang sama seperti kegagalan lain.

Menangkap Induk Berarti Menangkap Semua Anak #

Aturan paling praktis dari hierarki ini adalah: blok catch menerima objek yang instanceof tipenya, dan setiap anak adalah instance dari induknya. Kita bisa membuktikannya langsung pada satu objek DivisionByZeroError:

php
<?php
try {
    $sisa = 10 % 0;
    echo $sisa;
} catch (Throwable $e) {
    $tipeCek = [
        "Throwable", "Error", "ArithmeticError", "DivisionByZeroError", "Exception",
    ];
    foreach ($tipeCek as $tipe) {
        echo "$tipe: " . var_export($e instanceof $tipe, true) . "\n";
    }
}

Output yang Dihasilkan:

text
Throwable: true
Error: true
ArithmeticError: true
DivisionByZeroError: true
Exception: false

Objek DivisionByZeroError menjawab true untuk empat tipe pertama karena berada di jalur Throwable → Error → ArithmeticError → DivisionByZeroError. Jawaban false untuk Exception menegaskan bahwa kedua cabang besar memang tidak pernah bersilang. Konsekuensinya, catch (Throwable $e) adalah jaring pengaman yang menangkap apa pun, sedangkan catch (Error $e) dan catch (Exception $e) masing-masing hanya mengunci satu cabang.

Urutan blok catch juga mengikuti aturan hierarki: PHP memeriksa blok dari atas ke bawah dan menjalankan blok pertama yang cocok, lalu berhenti. Karena itu, tipe spesifik harus ditulis sebelum tipe induknya:

php
<?php
function namaBulan(int $nomor): string
{
    $bulan = [
        1 => "Januari", 2 => "Februari", 3 => "Maret", 4 => "April",
        5 => "Mei", 6 => "Juni", 7 => "Juli", 8 => "Agustus",
        9 => "September", 10 => "Oktober", 11 => "November", 12 => "Desember",
    ];

    if (!isset($bulan[$nomor])) {
        throw new OutOfRangeException("Nomor bulan tidak valid: $nomor");
    }

    return $bulan[$nomor];
}

try {
    echo namaBulan(13) . "\n";
} catch (OutOfRangeException $e) {
    echo "Tangkapan spesifik: " . $e->getMessage() . "\n";
} catch (LogicException $e) {
    echo "Tangkapan induk: " . $e->getMessage() . "\n";
}

Output yang Dihasilkan:

text
Tangkapan spesifik: Nomor bulan tidak valid: 13

OutOfRangeException adalah cucu dari LogicException, sehingga kedua blok sama-sama cocok. Blok spesifik yang lebih dulu menang, mencetak tangkapan spesifik, dan blok kedua diabaikan. Jika urutannya dibalik, blok LogicException akan menang lebih dulu dan blok spesifik tidak akan pernah terpakai; PHP tidak menganggap urutan seperti itu sebagai error, sehingga kesalahan urutan hanya terlihat dari perilaku program. Versi ringkasnya:

php
<?php
try {
    throw new OutOfRangeException("Nomor bulan tidak valid: 13");
} catch (LogicException $e) {
    echo "Ditangkap sebagai LogicException, tipe asli: " . get_class($e) . "\n";
}

Output yang Dihasilkan:

text
Ditangkap sebagai LogicException, tipe asli: OutOfRangeException

Satu blok catch (LogicException $e) cukup untuk menangkap OutOfRangeException, InvalidArgumentException, DomainException, dan seluruh keponakannya. Tipe asli objek tidak hilang; method get_class() dan operator instanceof selalu bisa mengungkapnya kembali di dalam blok.

Ringkasan #

Pohon Throwable terbagi menjadi cabang Error untuk kode yang rusak dan cabang Exception untuk kegagalan yang dilempar kode secara eksplisit, masing-masing dengan turunan yang spesifik. Menangkap sebuah tipe berarti menangkap seluruh turunannya, dan blok catch pertama yang cocok selalu menang. Dengan peta ini kita siap membahas pertanyaan yang muncul sejak awal section: kapan sebaiknya kita memakai error dan kapan exception? Chapter berikutnya membandingkan kedua mekanisme tersebut secara berdampingan.

Referensi #