Error handling adalah cara programmu bereaksi saat sesuatu gagal, tanpa ikut tumbang. Bayangin Warung Bu Tini lagi ramai. Pelayan mencatat "Es Teh 2 gelas", lalu sadar stoknya habis. Dia gak diam membeku: dia melempar kabar itu ke depan, dan kasir yang siap menangkapnya bilang ke pembeli "Maaf, Es Teh habis". Kalau gak ada yang menangkap, warung tutup mendadak. Dan apa pun yang terjadi, struknya tetap ditutup. Di PHP, kabar itu sebuah exception: melemparnya dengan throw, menangkapnya dengan try/catch, dan "apa pun yang terjadi" itu finally.
🧪 Tebakan cepat: apa output dari
try { throw new Exception("Gagal"); echo "A"; } catch (Exception $e) { echo "B"; } finally { echo "C"; }? Tebak dulu, jawabannya ada di bagian "Urutan try, catch, finally".
throw: melempar masalah
Ini pesan.php, pencatat pesanan Warung Bu Tini:
<?php
class StokHabisException extends RuntimeException {}
function pesan(string $menu, int $jumlah): string {
$stok = ["Nasi Goreng" => 3, "Es Teh" => 0];
if ($jumlah < 1) {
throw new InvalidArgumentException("Jumlah minimal 1, bukan $jumlah");
}
if ($stok[$menu] < $jumlah) {
throw new StokHabisException("Stok $menu habis");
}
return "$jumlah $menu dicatat";
}
echo "Mulai\n";
echo pesan("Es Teh", 2) . "\n";
echo "Selesai\n";
throw new ...membuat objek exception lalu melemparnya. Fungsi berhenti di baris itu,returndi bawahnya gak pernah dicapai.- Teks di dalam kurung disimpan di exception-nya, nanti dibaca dengan
$e->getMessage(). InvalidArgumentExceptiondanRuntimeExceptionsudah disediakan PHP.StokHabisExceptiondi baris 2 buatan kita sendiri.
Gak ada yang menangkap, jadi PHP berhenti dan menulis:
$ php pesan.php
Mulai
PHP Fatal error: Uncaught StokHabisException: Stok Es Teh habis in /app/pesan.php:10
Stack trace:
#0 /app/pesan.php(16): pesan('Es Teh', 2)
#1 {main}
thrown in /app/pesan.php on line 10
Cara membacanya: nama class exception dan pesannya, lalu file dan baris tempat throw (baris 10). Stack trace adalah jejak panggilan: #0 berarti baris 16 memanggil pesan('Es Teh', 2), dan #1 {main} adalah kode utama file. "Selesai" gak pernah tercetak. Awalan PHP Fatal error: itu versi log dan terminal; kalau display_errors menyala, teks yang sama juga tampil sebagai Fatal error: Uncaught ....
try dan catch: menangkap
echo "Mulai\n";
try {
echo pesan("Es Teh", 2) . "\n";
echo "Lanjut\n";
} catch (StokHabisException $e) {
echo "Maaf: " . $e->getMessage() . "\n";
} finally {
echo "Struk ditutup\n";
}
echo "Selesai\n";
Mulai
Maaf: Stok Es Teh habis
Struk ditutup
Selesai
try { ... }membungkus kode yang mungkin melempar exception.- Begitu ada
throw, sisa bloktrydilompati. "Lanjut" gak tercetak. catch (StokHabisException $e)menangkap exception jenis itu, dan$eadalah objeknya.finallyselalu jalan, ada exception atau gak.- Sesudah ditangkap, program lanjut seperti biasa: "Selesai" tercetak.
Urutan try, catch, finally
- Gak ada
throw: seluruhtryjalan,catchdilewati, lalufinally. - Ada
throwyang cocok dengancatch: sisatrydilompati,catchjalan, lalufinally, lalu program lanjut. - Ada
throwyang gak cocok:finallytetap jalan dulu, baru program tumbang dengan Fatal error. Misalnyapesan("Nasi Goreng", 0)mencetak "Mulai" dan "Struk ditutup", laluUncaught InvalidArgumentException, karenacatchdi atas cuma menangkapStokHabisException.
Jadi di tebakan tadi throw langsung melompat ke catch, echo "A" dilewati, catch mencetak B, dan finally mencetak C. Outputnya BC. Itu jawaban tebakannya.
try wajib punya minimal satu catch atau finally. Tanpa keduanya, file ditolak sebelum satu baris pun jalan: PHP Fatal error: Cannot use try without catch or finally.
Exception dan Error: dua cabang Throwable
Semua yang bisa di-throw mengimplementasikan interface Throwable (interface yang kamu kenal di OOP Lanjutan). Cabangnya dua:
Throwable
├── Exception: masalah yang kamu duga (input salah, stok habis)
│ ├── LogicException
│ │ └── InvalidArgumentException
│ └── RuntimeException
│ └── StokHabisException (buatan kita)
└── Error: kesalahan yang dilempar PHP sendiri
├── TypeError
├── ValueError
└── ArithmeticError
└── DivisionByZeroError
catch menangkap class yang disebut dan semua turunannya. catch (Exception $e) menangkap stok habis dan jumlah nol, tapi gak menangkap Error. Contohnya pesan("Nasi Goreng", "dua"): "dua" bukan angka untuk int $jumlah (tipe parameter dari lesson Fungsi), jadi PHP melempar TypeError, dan itu lolos dari catch (Exception $e):
try {
echo pesan("Nasi Goreng", "dua") . "\n";
} catch (Exception $e) {
echo "Exception: " . $e->getMessage() . "\n";
} catch (Throwable $e) {
echo get_class($e) . ": " . $e->getMessage() . "\n";
}
TypeError: pesan(): Argument #2 ($jumlah) must be of type int, string given, called in /app/pesan.php on line 16
catch dicek dari atas ke bawah dan yang pertama cocok yang dipakai, jadi tulis yang paling spesifik duluan. Pembagian juga begitu: di PHP 8, 30000 / 0 melempar DivisionByZeroError: Division by zero. Karena itu fungsi yang membagi biasanya mengecek pembaginya dulu, lalu melempar InvalidArgumentException dengan pesan yang jelas, misalnya Tidak bisa membagi dengan nol. Pembagian yang wajar tetap mengembalikan hasilnya: 10 / 2 jadi 5.
Exception buatan sendiri
class StokHabisException extends RuntimeException {}
Satu baris sudah cukup: dia mewarisi semua isi RuntimeException, termasuk pesan dan getMessage(). Manfaatnya ada di nama: catch (StokHabisException $e) cuma menangkap stok habis, bukan masalah lain. Akhiri namanya dengan Exception, dan turunkan dari RuntimeException (masalah saat program jalan) atau LogicException (kode dipakai dengan salah).
💡 Lempar ulang dengan previous
Kadang kamu menangkap satu exception, lalu melempar yang lebih jelas untuk kode di atasnya. Simpan yang lama lewat previous, supaya penyebab aslinya gak hilang:
class PesananGagalException extends Exception {}
try {
pesan("Es Teh", 2);
} catch (StokHabisException $e) {
throw new PesananGagalException("Pesanan meja 4 gagal", previous: $e);
}
PHP Fatal error: Uncaught StokHabisException: Stok Es Teh habis in /app/pesan.php:10
Stack trace:
#0 /app/pesan.php(18): pesan('Es Teh', 2)
#1 {main}
Next PesananGagalException: Pesanan meja 4 gagal in /app/pesan.php:20
Stack trace:
#0 {main}
thrown in /app/pesan.php on line 20
PHP mencetak rantainya dari penyebab pertama, lalu Next untuk yang membungkusnya. Di dalam catch, $e->getPrevious()->getMessage() membaca pesan aslinya.
💡 Warning bukan exception
pesan("Soto", 1) membaca $stok["Soto"] yang gak ada, dan PHP cuma memberi peringatan:
PHP Warning: Undefined array key "Soto" in /app/pesan.php on line 9
Warning dicetak, lalu program jalan terus dengan nilai null, dan try/catch gak bisa menangkapnya. Di sini null < 1 bernilai true, jadi yang keluar Stok Soto habis, pesan yang menyesatkan. Cek dulu dengan isset($stok[$menu]), lalu lempar exception yang jujur.
⚠️ Jebakan yang sering ketemu
catch (Exception $e)dikira menangkap semuanya.TypeErrordanDivisionByZeroErrorturunanError, jadi lolos. PakaiThrowablekalau memang mau menangkap semua.- Mencetak pesan tanpa
throw.echo "Tidak bisa membagi dengan nol";lalu lanjut menghitung bukan error handling: pemanggilnya gak tahu ada yang gagal, dancatchdi luar gak pernah jalan. Lempar denganthrow new. - Fungsi bertipe kembalian yang kosong.
function bagi(int $a, int $b): float {}tanpareturnmelemparTypeError: bagi(): Return value must be of type float, none returned. catchkosong.catch (Exception $e) {}menelan masalahnya diam-diam. Minimal tampilkan atau catat pesannya.
TL;DR: throw new NamaException("pesan") menghentikan fungsi dan melempar masalahnya. try { ... } catch (Jenis $e) { ... } menangkapnya: sisa try dilompati, catch jalan, dan $e->getMessage() membaca pesannya. finally selalu jalan, bahkan sebelum Fatal error. Semua yang dilempar adalah Throwable, dengan dua cabang: Exception (masalah yang kamu duga) dan Error (TypeError, DivisionByZeroError), dan catch (Exception $e) gak menangkap Error. Exception buatan sendiri cukup class XException extends RuntimeException {}, dan previous menyimpan penyebab aslinya. Exception yang lolos berakhir dengan PHP Fatal error: Uncaught ... beserta stack trace, sedangkan Warning cuma peringatan yang gak bisa ditangkap.