Melempar exception hanyalah separuh cerita; tanpa penangkap, object kegagalan berakhir sebagai fatal error Uncaught. PHP menyediakan struktur try-catch-finally sebagai mekanisme penangkapan yang terstruktur: try menjalankan kode berisiko, catch menangani kegagalan berdasarkan tipe, dan finally menjamin pembersihan selalu terjadi. Pada chapter ini kita berlatih seluruh pola tersebut — dari penangkapan dasar, urutan blok catch, multi-catch, hingga try bersarang dan melempar ulang exception.
Struktur Dasar try-catch #
Blok try membungkus kode yang berpotensi melempar exception. Ketika throw terjadi di dalamnya, PHP menghentikan sisa kode try, lalu mencocokkan object yang dilempar dengan tipe yang dideklarasikan tiap blok catch — cocok berarti object tersebut adalah instance dari tipe itu atau turunannya:
<?php
function validasiStok(int $pesanan, int $stok) {
if ($pesanan > $stok) {
throw new Exception("Stok tidak cukup, sisa $stok");
}
echo "Pesanan diproses\n";
}
try {
validasiStok(5, 3);
echo "Baris ini dilewati\n";
} catch (Exception $e) {
echo "Gagal: " . $e->getMessage() . "\n";
}
echo "Program berlanjut\n";Output yang Dihasilkan:
Gagal: Stok tidak cukup, sisa 3
Program berlanjutMari kita telusuri alurna baris demi baris:
- Baris 4:
validasiStok(5, 3)mendapati pesanan melebihi stok, sehingga objectExceptiondengan pesan"Stok tidak cukup, sisa 3"dilempar. - Baris 6 dan 11: kedua baris ini tidak pernah tereksekusi —
echo "Pesanan diproses"di dalam function danecho "Baris ini dilewati"di dalamtrysama-sama dilewati begituthrowterjadi. - Baris 12-14: blok
catch (Exception $e)cocok dengan object yang dilempar, pesan dibaca lewatgetMessage(), lalu eksekusi normal dilanjutkan ke baris 15.
Perhatikan hasil akhirnya: program tidak mati, melainkan mencatat kegagalan lalu lanjut — inilah perbedaan mendasar antara exception yang ditangkap dan fatal error.
Penangkatan Berdasarkan Tipe #
Pencocokan catch didasarkan pada tipe object, dan tipe yang dideklarasikan harus benar-benar berkerabat. Sebelum contoh, kita perlu mengenal dua subclass bawaan yang akan dipakai: RuntimeException (kegagalan saat runtime, turunan langsung Exception) dan InvalidArgumentException (argumen tidak valid, turunan LogicException yang juga turunan Exception). Keduanya sering dipakai karena lebih ekspresif daripada Exception polos. Kini mari kita lihat apa yang terjadi ketika tipe penangkap tidak cocok:
<?php
try {
throw new RuntimeException("Layanan tidak merespons");
} catch (InvalidArgumentException $e) {
echo "Tidak akan pernah ke sini\n";
}
echo "Program berlanjut\n";Output yang Dihasilkan:
Fatal error: Uncaught RuntimeException: Layanan tidak merespons in /home/z/my-project/php-course/content/php/error-handling/try-catch-finally/.verify_tmp.php:3
Stack trace:
#0 {main}
thrown in /home/z/my-project/php-course/content/php/error-handling/try-catch-finally/.verify_tmp.php on line 3Pada contoh di atas, object RuntimeException yang dilempar di baris 3 tidak cocok dengan catch (InvalidArgumentException $e) — RuntimeException bukan InvalidArgumentException maupun turunannya. Karena tidak ada penangkap lain, exception lolos keluar dari try dan berakhir sebagai Fatal error: Uncaught RuntimeException, dengan baris 6 dan 7 yang tidak pernah tereksekusi.
Urutan Beberapa Blok catch #
Satu try boleh diikuti beberapa blok catch dengan tipe berbeda, dan PHP memeriksa kecocokan dari atas ke bawah. Aturan praktisnya: letakkan tipe paling spesifik di urutan paling atas, seperti berikut:
<?php
try {
throw new InvalidArgumentException("Format email tidak valid");
} catch (InvalidArgumentException $e) {
echo "Spesifik: " . $e->getMessage() . "\n";
} catch (Exception $e) {
echo "Umum: " . $e->getMessage() . "\n";
}Output yang Dihasilkan:
Spesifik: Format email tidak validObject InvalidArgumentException yang dilempar di baris 3 diperiksa terhadap catch pertama, cocok, dan ditangani di sana — blok catch (Exception $e) kedua hanya akan bekerja untuk exception di luar InvalidArgumentException. Bandingkan dengan urutan yang terbalik:
<?php
try {
throw new InvalidArgumentException("Format email tidak valid");
} catch (Exception $e) {
echo "Umum: " . $e->getMessage() . "\n";
} catch (InvalidArgumentException $e) {
echo "Spesifik: blok ini tidak pernah tereksekusi\n";
}Output yang Dihasilkan:
Umum: Format email tidak validKali ini blok catch (Exception $e) yang pertama langsung cocok karena InvalidArgumentException adalah turunan Exception, sehingga blok spesifik di bawahnya tidak pernah sempat diperiksa. Berbeda dari Java yang menolak kode semacam ini saat kompilasi, PHP membiarkan blok bayangan tersebut tanpa peringatan apa pun — kesalahan urutan seperti ini hanya akan terasa saat penanganan khusus Anda diam-diam tidak pernah berjalan, jadi biasakan menempatkan tipe paling spesifik pertama.
Multi-catch dengan Tipe Gabungan #
Sering kali beberapa tipe exception ingin diperlakukan dengan cara yang sama. Sejak PHP 7.1, satu blok catch dapat menerima beberapa tipe sekaligus lewat operator pipe (|):
<?php
function bacaPengaturan(string $sumber) {
if ($sumber === "file") {
throw new RuntimeException("Berkas pengaturan hilang");
}
throw new InvalidArgumentException("Sumber $sumber tidak dikenal");
}
foreach (["file", "api"] as $sumber) {
try {
bacaPengaturan($sumber);
} catch (RuntimeException | InvalidArgumentException $e) {
echo "[$sumber] " . $e->getMessage() . "\n";
}
}Output yang Dihasilkan:
[file] Berkas pengaturan hilang
[api] Sumber api tidak dikenalMari kita telaah contoh di atas:
- Baris 3-6: function
bacaPengaturan()melemparRuntimeExceptionuntuk sumber"file"danInvalidArgumentExceptionuntuk sumber lain. - Baris 13 (
catch (RuntimeException | InvalidArgumentException $e)): satu blok yang sama menangkap kedua tipe, sehingga kita tidak perlu menduplikasi logika penanganan ke dua blok terpisah. - Baris 9-15: loop mencoba kedua sumber; masing-masing kegagalan tertangkap dan pesannya tercetak dengan awalan sumbernya, tanpa menghentikan iterasi berikutnya.
finally Selalu Tereksekusi #
Blok finally adalah jaminan: kodenya dieksekusi apa pun hasil blok try — berhasil mulus, tertangkap catch, maupun dilempar keluar. Bahkan pernyataan return di dalam try tidak bisa melompatinya, karena finally berjalan sebelum nilai kembali benar-benar diserahkan ke pemanggil:
<?php
function cekSaldo(float $saldo): string {
try {
if ($saldo < 0) {
return "Saldo negatif terdeteksi";
}
return "Saldo valid";
} finally {
echo "Blok finally selalu tereksekusi\n";
}
}
echo cekSaldo(-50000) . "\n";
echo cekSaldo(150000) . "\n";Output yang Dihasilkan:
Blok finally selalu tereksekusi
Saldo negatif terdeteksi
Blok finally selalu tereksekusi
Saldo validPerhatikan urutan pada output di atas: untuk setiap pemanggilan, teks dari finally tercetak lebih dulu, baru diikuti nilai kembalian cekSaldo(). Eksekusi return di baris 4 maupun baris 6 sama-sama ditunda hingga finally selesai — itulah mengapa kedua pemanggilan menampilkan pola yang konsisten. Perilaku yang sama berlaku saat exception lewat, sebelum object tersebut melanjutkan perjalanan keluar:
<?php
function prosesPesanan(string $menu) {
try {
throw new Exception("Menu $menu sedang habis");
} finally {
echo "Membersihkan keranjang belanja\n";
}
}
try {
prosesPesanan("Rendang");
} catch (Exception $e) {
echo "Ditangkap di luar: " . $e->getMessage() . "\n";
}Output yang Dihasilkan:
Membersihkan keranjang belanja
Ditangkap di luar: Menu Rendang sedang habisPada contoh di atas, throw di baris 3 melewati finally di baris 5 terlebih dahulu — pembersihan keranjang tereksekusi — sebelum exception melanjutkan perjalanan dan ditangkap catch di lapisan luar. Pola ini adalah tempat yang tepat untuk meletakkan pembersihan: menutup koneksi database, melepas kunci berkas, atau mencatat aktivitas terakhir.
Jaring Pengaman catch (Throwable) #
Kita sudah melihat bahwa catch (Exception $e) tidak menangkap Error dari engine. Ketika sebuah titik program harus tetap hidup menghadapi kegagalan apa pun — misalnya loop pemrosesan data — kita bisa menangkap interface Throwable yang menaungi kedua cabang hierarki:
<?php
function hitungRataRata(array $nilai): float {
return array_sum($nilai) / count($nilai);
}
try {
echo "Rata-rata: " . hitungRataRata([]) . "\n";
} catch (Throwable $e) {
echo "Kegagalan tertangkap: " . $e->getMessage() . "\n";
}
echo "Program tetap berlanjut\n";Output yang Dihasilkan:
Kegagalan tertangkap: Division by zero
Program tetap berlanjutPada contoh di atas, array kosong membuat count($nilai) bernilai 0, sehingga pembagian di baris 3 memicu DivisionByZeroError — sebuah Error, bukan Exception. Blok catch (Throwable $e) tetap menangkapnya karena Error maupun Exception sama-sama menerapkan interface Throwable, lalu program berlanjut ke baris terakhir. Gunakan pola ini secara selektif di titik-titik yang benar-benar membutuhkan ketahanan: menelan semua kegagalan tanpa mencatatnya justru menyembunyikan bug, sehingga jaring pengaman seperti ini selayaknya selalu disertai pencatatan ke log.
try Bersarang dan Melempar Ulang #
Blok try dapat bersarang: exception yang tidak tertangkap di try bagian dalam akan merambat keluar dan diperiksa kembali oleh try lapisan luarnya. Pola ini biasa dipakai bersama rethrow — menangkap lalu melempar ulang dengan konteks tambahan. Konstruktor Exception menerima argumen ketiga berupa exception sebelumnya, sehingga kegagalan asli tidak hilang dan bisa dibaca lewat getPrevious():
<?php
try {
try {
throw new Exception("Data email kosong", 422);
} catch (Exception $inner) {
throw new Exception("Validasi gagal: " . $inner->getMessage(), 0, $inner);
}
} catch (Exception $outer) {
echo "Pesan luar : " . $outer->getMessage() . "\n";
echo "Pesan asal : " . $outer->getPrevious()->getMessage() . "\n";
}Output yang Dihasilkan:
Pesan luar : Validasi gagal: Data email kosong
Pesan asal : Data email kosongMari kita bedah alur bersarang pada contoh di atas:
- Baris 4:
trybagian dalam melemparExceptiondengan pesan"Data email kosong"dan kode422. - Baris 5-6:
catchbagian dalam menangkapnya sebagai$inner, lalu melempar exception baru dengan pesan berkonteks —"Validasi gagal: ..."— sambil menyematkan$innersebagai argumen ketiga konstruktor. - Baris 8-11:
trylapisan luar menangkap exception baru sebagai$outer;getPrevious()mengembalikan object asli$inner, sehingga pesan asal"Data email kosong"tetap bisa dibaca.
Rantai semacam ini menjaga keterangan kegagalan tetap utuh dari lapisan dalam ke lapisan luar. Teknik melempar ulang secara penuh — termasuk kapan rethrow tepat dilakukan — kita dalami pada chapter Throw dan Rethrow .
Berkas contoh.php pada bagian bawah halaman merangkum contoh-contoh utama di atas dalam satu program utuh yang dapat Anda unduh dan jalankan langsung.
Ringkasan #
Struktur try-catch menangkap exception berdasarkan tipe, dengan aturan menempatkan blok paling spesifik di urutan pertama karena PHP tidak memperingatkan blok yang tertutupi. Multi-catch catch (A | B $e) menyatukan penanganan beberapa tipe, catch (Throwable $e) menjadi jaring pengaman untuk kedua cabang hierarki, dan finally menjamin kode pembersihan selalu tereksekusi — bahkan saat try berisi return atau throw. Dengan pola bersarang dan perantai getPrevious(), konteks kegagalan dapat dilapiskan tanpa kehilangan informasi asli. Setelah pola penangkapan ini dikuasai, langkah alami berikutnya adalah membuat class exception sendiri agar setiap aturan bisnis punya tipe kegagalan yang khas.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
contoh.php
Unduh berkas<?php
// contoh.php — rangkuman contoh chapter "try-catch-finally".
// Jalankan dengan: php contoh.php
function validasiStok(int $pesanan, int $stok) {
if ($pesanan > $stok) {
throw new Exception("Stok tidak cukup, sisa $stok");
}
echo "Pesanan diproses\n";
}
try {
validasiStok(5, 3);
echo "Baris ini dilewati\n";
} catch (Exception $e) {
echo "Gagal: " . $e->getMessage() . "\n";
}
echo "Program berlanjut\n";
echo "---\n";
function bacaPengaturan(string $sumber) {
if ($sumber === "file") {
throw new RuntimeException("Berkas pengaturan hilang");
}
throw new InvalidArgumentException("Sumber $sumber tidak dikenal");
}
foreach (["file", "api"] as $sumber) {
try {
bacaPengaturan($sumber);
} catch (RuntimeException | InvalidArgumentException $e) {
echo "[$sumber] " . $e->getMessage() . "\n";
}
}
echo "---\n";
function cekSaldo(float $saldo): string {
try {
if ($saldo < 0) {
return "Saldo negatif terdeteksi";
}
return "Saldo valid";
} finally {
echo "Blok finally selalu tereksekusi\n";
}
}
echo cekSaldo(-50000) . "\n";
echo cekSaldo(150000) . "\n";
echo "---\n";
function hitungRataRata(array $nilai): float {
return array_sum($nilai) / count($nilai);
}
try {
echo "Rata-rata: " . hitungRataRata([]) . "\n";
} catch (Throwable $e) {
echo "Kegagalan tertangkap: " . $e->getMessage() . "\n";
}
echo "Program tetap berlanjut\n";