Pada chapter try-catch-finally kita berlatih menangkap exception yang sudah terlanjur terjadi. Namun try-catch hanyalah setengah dari cerita error handling: sebelum sebuah exception dapat ditangkap, harus ada kode yang melemparnya terlebih dahulu. Pada chapter ini kita fokus pada sisi pelempar, yaitu pernyataan throw dan teknik melempar ulang (rethrow) dengan konteks tambahan, sehingga kegagalan yang lahir di lapisan dalam aplikasi tetap informatif ketika sampai ke lapisan luar.
Melempar Exception di Titik Mana Pun #
Perintah throw adalah pernyataan (statement) yang segera menghentikan alur eksekusi normal. Ketika interpreter PHP mengeksekusi throw, interpreter menghentikan sisa kode pada function tersebut, lalu mencari blok catch yang cocok di call stack, mulai dari pemanggil terdekat hingga ke atas. Perhatikan tiga bentuk pemakaian throw berikut:
<?php
throw new RuntimeException("pesan"); // melempar objek baru
throw $exceptionYangAda; // melempar ulang objek yang sudah ada
throw new RuntimeException("pesan", 0, $penyebab); // melempar objek baru membawa penyebab
Ketiga bentuk di atas memakai constructor RuntimeException yang menerima tiga parameter: pesan, kode, dan exception penyebab (previous). Pernyataan throw boleh ditulis di mana pun pernyataan biasa ditulis: di dalam function, method, loop, blok if, bahkan di top-level script. Mari kita lihat pemakaian yang paling umum, yaitu menjaga kondisi tidak valid agar tidak diteruskan:
<?php
function hitungDiskon(float $harga, int $persen): float
{
if ($persen < 0 || $persen > 100) {
throw new InvalidArgumentException("Persentase diskon harus 0-100, diberikan: $persen");
}
return $harga * $persen / 100;
}
echo hitungDiskon(200000, 20) . "\n";
try {
hitungDiskon(200000, 150);
} catch (InvalidArgumentException $e) {
echo "Ditolak: " . $e->getMessage() . "\n";
}Output yang Dihasilkan:
40000
Ditolak: Persentase diskon harus 0-100, diberikan: 150Mari kita telaah baris-baris kode di atas:
- Baris 3-5 (
if (...) { throw ... }): functionhitungDiskon()menolak nilai$persendi luar rentang 0-100 sebelum perhitungan dilakukan. Eksekusi function berhenti tepat di baristhrow; barisreturntidak pernah tercapai untuk input tersebut. - Baris 9 (
echo hitungDiskon(200000, 20)): pemanggilan dengan input valid berjalan normal dan mencetak40000. - Baris 12 (
hitungDiskon(200000, 150)): pelemparan exception menghentikan pemanggilan, lalu blokcatchpada pemanggil menangkap objekInvalidArgumentExceptiontersebut dan mencetak pesannya.
Perhatikan juga bahwa pesan error dibuat informatif sejak awal: pesan menyebutkan aturan yang dilanggar dan nilai yang menjadi masalah. Pesan seperti ini jauh lebih mudah dianalisis dibandingkan pesan generik seperti "input salah".
throw sebagai Ekspresi #
Sebelum PHP 8.0, throw hanya boleh berdiri sebagai pernyataan lengkap dengan barisnya sendiri. Sejak PHP 8.0, throw juga merupakan ekspresi (throw expression), sehingga throw dapat muncul di tempat yang menuntut sebuah nilai, misalnya cabang ternary, cabang match, atau rantai null coalescing. Fitur ini membuat kode validasi ringkas terasa natural:
<?php
function usiaPendaftar(int $usia): int
{
return $usia >= 17 ? $usia : throw new InvalidArgumentException("Usia minimal 17 tahun, diberikan: $usia");
}
function tingkatAkses(string $peran): string
{
return match ($peran) {
"admin" => "akses penuh",
"editor" => "akses terbatas",
default => throw new RuntimeException("Peran tidak dikenal: $peran"),
};
}
echo usiaPendaftar(25) . "\n";
echo tingkatAkses("editor") . "\n";
try {
usiaPendaftar(15);
} catch (InvalidArgumentException $e) {
echo "Ditolak: " . $e->getMessage() . "\n";
}
try {
tingkatAkses("hacker");
} catch (RuntimeException $e) {
echo "Gagal: " . $e->getMessage() . "\n";
}Output yang Dihasilkan:
25
akses terbatas
Ditolak: Usia minimal 17 tahun, diberikan: 15
Gagal: Peran tidak dikenal: hackerMari kita bedah contoh di atas:
- Baris 3 (
return $usia >= 17 ? $usia : throw ...): ternary mengevaluasi kondisi terlebih dahulu. Jika kondisi bernilaitrue, hasil$usiadikembalikan; jika bernilaifalse, ekspresithrowdieksekusi dan seluruh function langsung dihentikan. - Baris 9-13 (
match): setiap lenganmatchharus menghasilkan sebuah nilai. Lengandefaultmemakaithrowsebagai nilainya, sehingga kondisi yang tidak dikenal tidak pernah menghasilkan nilai yang tidak valid. - Baris 16-27: kedua pemanggilan yang valid mencetak hasilnya, sedangkan pemanggilan yang melanggar aturan tertangkap oleh blok
catchmasing-masing.
Melempar Ulang dengan Konteks Tambahan #
Aplikasi nyata tersusun berlapis-lapis: function utilitas di lapisan dalam, function bisnis di lapisan tengah, dan handler HTTP di lapisan luar. Exception yang lahir di lapisan dalam sering kali berbicara dalam istilah teknis (misalnya format JSON rusak), sementara lapisan luar butuh kalimat berkonteks bisnis (misalnya profil pengguna gagal dimuat). Teknik rethrow menjembatani kedua kebutuhan tersebut: lapisan yang lebih tinggi menangkap exception lalu melempar ulang dalam bentuk exception baru yang membawa konteks, tanpa membuang exception aslinya.
<?php
function parseProfil(string $json): array
{
$data = json_decode($json, true);
if (!is_array($data)) {
throw new RuntimeException("Format JSON profil tidak valid.");
}
return $data;
}
function muatProfilPengguna(string $json): array
{
try {
return parseProfil($json);
} catch (RuntimeException $e) {
throw new RuntimeException("Gagal memuat profil pengguna.", 0, $e);
}
}
try {
muatProfilPengguna("{ json rusak");
} catch (RuntimeException $e) {
echo "Pesan utama: " . $e->getMessage() . "\n";
echo "Penyebab : " . $e->getPrevious()->getMessage() . "\n";
}Output yang Dihasilkan:
Pesan utama: Gagal memuat profil pengguna.
Penyebab : Format JSON profil tidak valid.Bagian kunci dari pola ini ada pada satu baris: throw new RuntimeException("Gagal memuat profil pengguna.", 0, $e). Parameter ketiga constructor menerima objek exception lama sebagai previous exception. Hasilnya, exception baru membawa pesan berkonteks bisnis, sementara exception lama tetap tersimpan utuh di dalamnya dan dapat diambil kembali melalui method getPrevious(). Kode 0 pada parameter kedua adalah error code; kita isi 0 karena contoh ini tidak membutuhkan kode khusus.
Menelusuri Rantai Penyebab dengan getPrevious #
Semakin panjang rantai rethrow, semakin dalam pula tumpukan penyebab yang tersimpan. Method getPrevious() mengembalikan exception penyebab, atau null jika tidak ada. Karena rantai penyebab berbentuk linked list, penelusuran paling natural dilakukan dengan loop:
<?php
$akar = new RuntimeException("Koneksi ke server database terputus.");
$lapis2 = new RuntimeException("Query data pesanan gagal dieksekusi.", 0, $akar);
$lapis1 = new RuntimeException("Halaman riwayat pesanan tidak dapat dirender.", 0, $lapis2);
$counter = 1;
for ($e = $lapis1; $e !== null; $e = $e->getPrevious()) {
echo "Lapisan $counter: " . $e->getMessage() . "\n";
$counter++;
}Output yang Dihasilkan:
Lapisan 1: Halaman riwayat pesanan tidak dapat dirender.
Lapisan 2: Query data pesanan gagal dieksekusi.
Lapisan 3: Koneksi ke server database terputus.Loop for di atas bergerak dari exception paling luar menuju exception paling dalam. Pada iterasi pertama variabel $e berisi $lapis1 (halaman gagal dirender), pada iterasi kedua $lapis2 (query gagal), dan pada iterasi ketiga $akar (koneksi terputus) yang tidak lagi memiliki penyebab, sehingga iterasi berhenti setelah $akar dicetak. Pola penelusuran seperti ini sangat berguna di logging: satu baris log dapat memuat seluruh rantai kegagalan, dari gejala yang terlihat pengguna hingga akar masalahnya.
Kapan Melempar Ulang dan Kapan Cukup Ditangani #
Melempar ulang bukan tindakan baku untuk semua catch block. Keputusannya bergantung pada satu pertanyaan: apakah pemanggil di atas masih perlu mengetahui kegagalan tersebut? Tabel berikut merangkum situasi yang paling sering dijumpai:
| Situasi | Tindakan yang tepat |
|---|---|
| Kegagalan bisa dipulihkan di tempat (nilai default, input ulang) | Tangani di dalam catch, jangan lempar ulang |
catch hanya untuk mencatat log, keputusan tetap milik pemanggil | Catat log lalu throw $e (lempar ulang objek sama) |
| Pesan teknis perlu diperkaya konteks lapisan luar | Bungkus dengan exception baru + $previous |
| Kegagalan tidak relevan bagi alur utama (misal cache gagal di-refresh) | Tangani dengan diam setelah memastikan aman |
Contoh berikut menunjukkan pola “catat lalu lempar ulang objek yang sama”, yaitu throw $e tanpa membuat objek baru:
<?php
function prosesPembayaran(int $totalSen): void
{
try {
if ($totalSen < 100) {
throw new InvalidArgumentException("Total pembayaran minimal 100 sen.");
}
echo "Pembayaran diproses: $totalSen sen\n";
} catch (InvalidArgumentException $e) {
// titik pencatatan: di aplikasi nyata, tulis ke log di sini
throw $e;
}
}
try {
prosesPembayaran(25000);
prosesPembayaran(50);
} catch (InvalidArgumentException $e) {
echo "Pemanggil menerima: " . $e->getMessage() . "\n";
}Output yang Dihasilkan:
Pembayaran diproses: 25000 sen
Pemanggil menerima: Total pembayaran minimal 100 sen.Pada pemanggilan pertama, pembayaran tervalidasi dan diproses. Pada pemanggilan kedua, function prosesPembayaran() menolak nilai di bawah batas minimal, sempat ditangkap oleh catch milik function yang sama untuk keperluan pencatatan, lalu objek exception yang sama diteruskan ke atas melalui throw $e. Pemanggil tetap menerima exception dengan pesan asli, tanpa konteks yang hilang.
Ringkasan #
Perintah throw menghentikan eksekusi normal dan menyerahkan kendali ke blok catch terdekat yang cocok. Sejak PHP 8.0, throw juga berperan sebagai ekspresi yang bisa hidup di dalam ternary dan match. Teknik rethrow memperkaya pesan kegagalan dengan konteks lapisan luar sambil mempertahankan exception asli lewat parameter previous dan method getPrevious(). Dengan pola ini, satu jenis exception bawaan seperti RuntimeException ternyata bisa dipakai untuk banyak keperluan sekaligus. Pada chapter berikutnya kita akan belajar membangun jenis exception sendiri (custom exception) agar setiap kegagalan domain memiliki tipe yang spesifik dan mudah dipilah.