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:
| Class | Pemicu |
|---|---|
TypeError | Tipe argumen atau nilai kembali tidak cocok dengan deklarasi |
ArgumentCountError | Jumlah argumen pemanggilan kurang (turunan TypeError) |
ValueError | Nilai argumen di luar rentang yang diterima function |
ArithmeticError | Operasi aritmetika yang tidak dapat diselesaikan |
DivisionByZeroError | Pembagian atau modulo dengan nol (turunan ArithmeticError) |
ParseError | Sintaks tidak valid saat kompilasi, misalnya dari eval() |
UnhandledMatchError | Ekspresi 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
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:
42
Tertangkap TypeError: kaliDua(): Argument #1 ($angka) must be of type int, string givenPemanggilan 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
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:
25
Tertangkap ArithmeticError: DivisionByZeroError - Division by zeroKarena 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
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:
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:
| Class | Induk | Kegunaan |
|---|---|---|
LogicException | Exception | Induk keluarga kesalahan logika program |
BadFunctionCallException | LogicException | Function dipanggil dengan cara yang tidak benar |
BadMethodCallException | LogicException | Method dipanggil dengan cara yang tidak benar |
DomainException | LogicException | Nilai melanggar aturan domain bisnis |
InvalidArgumentException | LogicException | Argumen tidak sesuai kontrak function |
LengthException | LogicException | Panjang ilegal, misalnya string melebihi batas |
OutOfRangeException | LogicException | Permintaan nilai di luar jangkauan yang valid |
RuntimeException | Exception | Induk keluarga kegagalan saat eksekusi |
OutOfBoundsException | RuntimeException | Nilai kunci ilegal saat runtime |
OverflowException | RuntimeException | Menambah elemen ke wadah yang sudah penuh |
RangeException | RuntimeException | Nilai di luar rentang saat runtime |
UnderflowException | RuntimeException | Operasi pada wadah yang kosong |
UnexpectedValueException | RuntimeException | Nilai 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
try {
json_decode("{ json rusak", flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
echo "Tertangkap JsonException: " . $e->getMessage() . "\n";
}Output yang Dihasilkan:
Tertangkap JsonException: Syntax errorTanpa 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
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:
Throwable: true
Error: true
ArithmeticError: true
DivisionByZeroError: true
Exception: falseObjek 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
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:
Tangkapan spesifik: Nomor bulan tidak valid: 13OutOfRangeException 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
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:
Ditangkap sebagai LogicException, tipe asli: OutOfRangeExceptionSatu 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.