Blok try-catch menangkap exception pada titik yang kita pilih sendiri — tetapi hanya jika kita sempat memasangnya di sana. Aplikasi nyata memiliki ribuan baris kode, dan cukup satu lemparan exception yang tidak terduga di lokasi yang tidak terpikirkan untuk membuat seluruh halaman runtuh menjadi Fatal error: Uncaught ... lengkap dengan stack trace yang telanjang. PHP menyediakan set_exception_handler() sebagai jaring pengaman terakhir: satu callback global yang dipanggil ketika exception benar-benar terlepas tanpa ada catch yang menangkapnya. Chapter ini membahas cara kerjanya, perbedaan mendasarnya dari try-catch, dan kapan pemakaiannya memang tepat.

Saat Exception Tidak Ada yang Menangkap #

Mari kita lihat dulu kejadian lengkapnya ketika sebuah exception terlepas:

php
<?php
function ambilKonfigurasi(string $kunci): string
{
    throw new RuntimeException("Kunci konfigurasi '$kunci' tidak ditemukan");
}

echo "Membaca konfigurasi...\n";
echo ambilKonfigurasi("database_host") . "\n";
echo "Baris ini tidak pernah tercetak\n";

Output yang Dihasilkan:

text
Membaca konfigurasi...

Fatal error: Uncaught RuntimeException: Kunci konfigurasi 'database_host' tidak ditemukan in /home/z/my-project/php-course/content/php/error-handling/exception-handlers/.verify_tmp.php:4
Stack trace:
#0 /home/z/my-project/php-course/content/php/error-handling/exception-handlers/.verify_tmp.php(8): ambilKonfigurasi('database_host')
#1 {main}
  thrown in /home/z/my-project/php-course/content/php/error-handling/exception-handlers/.verify_tmp.php on line 4

Mari kita bedah laporan fatal di atas bagian demi bagian:

  1. Fatal error: Uncaught RuntimeException — kata Uncaught berarti exception ini dilempar tanpa pernah tertangkap catch mana pun, sehingga PHP menganggapnya kegagalan fatal.
  2. Pesan dan lokasi lempar — Kunci konfigurasi 'database_host' tidak ditemukan dilempar dari dalam ambilKonfigurasi() pada baris 4 berkas tempat throw berada.
  3. Stack trace: — jejak pemanggilan saat kejadian: #0 menunjukkan pemanggil ambilKonfigurasi('database_host') di baris 8, dan #1 {main} berarti rantai berakhir di level teratas program.
  4. Exit code 255 — kode keluar khusus yang menandakan program berakhir karena error; baris setelah pemanggilan tidak pernah dieksekusi.

Memasang Jaring Pengaman dengan set_exception_handler #

Fungsi set_exception_handler() menerima satu callable yang akan dipanggil PHP dengan objek exception yang terlepas sebagai satu-satunya argumen. Gunakan type declaration Throwable agar handler menangkap seluruh keluarga kegagalan — baik Exception maupun Error:

php
<?php
function tanganiException(Throwable $e): void
{
    echo "TERJADI MASALAH: " . $e->getMessage() . "\n";
    echo "Kelas exception: " . get_class($e) . "\n";
}

set_exception_handler('tanganiException');

throw new RuntimeException("Koneksi ke database gagal");

echo "Baris ini tidak pernah tercetak\n";

Output yang Dihasilkan:

text
TERJADI MASALAH: Koneksi ke database gagal
Kelas exception: RuntimeException

Alih-alih laporan fatal yang panjang, output kini berisi dua baris milik handler kita: pesan exception dan nama kelasnya dari get_class(). Handler menerima kendali penuh atas apa yang ditampilkan, dicatat, atau dikirim sebagai notifikasi terakhir.

Tumpukan Handler dan restore_exception_handler #

Sama seperti error handler, exception handler juga disimpan dalam tumpukan. Setiap set_exception_handler() menambah handler baru yang aktif, dan restore_exception_handler() melepas handler teratas untuk kembali ke handler sebelumnya:

php
<?php
function handlerCadangan(Throwable $e): void
{
    echo "[CADANGAN] " . $e->getMessage() . "\n";
}

function handlerUtama(Throwable $e): void
{
    echo "[UTAMA] " . $e->getMessage() . "\n";
}

set_exception_handler('handlerCadangan');
set_exception_handler('handlerUtama');

restore_exception_handler();

throw new RuntimeException("contoh kegagalan");

Output yang Dihasilkan:

text
[CADANGAN] contoh kegagalan

handlerUtama dipasang terakhir sehingga menempati posisi teratas, tetapi restore_exception_handler() langsung melepasnya sebelum exception terjadi. Akibatnya handlerCadangan — yang dipasang lebih dulu — yang menerima lemparan RuntimeException tersebut. Pola ini berguna ketika satu bagian program (misalnya skrip impor data) ingin memasang kebijakan penanganan terakhirnya sendiri tanpa menghapus kebijakan global aplikasi.

Perbedaan Mendasar dengan try-catch #

Sekilas handler exception terlihat seperti catch global, tetapi keduanya menjawab kebutuhan yang berbeda. Perbedaan paling menentukan: try-catch memungkinkan program berlanjut, sedangkan handler exception selalu menjadi titik akhir. Bandingkan kedua mekanisme berikut:

Aspektry-catchset_exception_handler()
CakupanHanya exception yang dilempar di dalam blok trySemua exception yang terlepas, di mana pun
Eksekusi setelahnyaBerlanjut ke kode setelah catchProgram berhenti setelah handler selesai
PemulihanBisa — alur kembali terkontrolTidak — hanya penanganan terakhir
Cocok untukKegagalan yang bisa dipulihkanPencatatan akhir dan pesan perpisahan

Perbedaan itu terlihat nyata bila keduanya dipasang dalam satu program:

php
<?php
function handlerTerakhir(Throwable $e): void
{
    echo "[HANDLER GLOBAL] " . $e->getMessage() . "\n";
}

set_exception_handler('handlerTerakhir');

try {
    throw new RuntimeException("masalah yang masih bisa dipulihkan");
} catch (RuntimeException $e) {
    echo "[CATCH] dipulihkan: " . $e->getMessage() . "\n";
}

echo "Eksekusi berlanjut sampai akhir program\n";

Output yang Dihasilkan:

text
[CATCH] dipulihkan: masalah yang masih bisa dipulihkan
Eksekusi berlanjut sampai akhir program

Exception yang dilempar di dalam try langsung tertangani blok catch, sehingga handler global tidak pernah terpanggil — handler hanya menerima exception yang benar-benar lolos dari semua catch. Setelah catch selesai, eksekusi berjalan terus sampai baris terakhir program. Aturan praktisnya: tangani kegagalan yang bisa dipulihkan dengan try-catch di tempat kejadiannya, dan sisakan handler global untuk yang tidak terduga.

Kapan Handler Ini Layak Dipasang #

Handler exception dipasang karena tiga alasan utama. Pertama, pencatatan terakhir: kegagalan yang sampai ke handler adalah bug yang tidak terpikirkan, jadi catat detail lengkapnya — kelas, pesan, berkas, baris, dan stack trace — ke log sebelum program berakhir, misalnya lewat error_log() sebagaimana dibahas pada chapter logging-errors . Kedua, pesan akhir yang layak: pada aplikasi web, handler adalah tempat menampilkan halaman error yang ramah kepada pengunjung alih-alih laporan fatal mentah. Ketiga, pekerjaan rumah terakhir: menutup koneksi atau membersihkan berkas sementara sebelum program benar-benar berhenti.

Yang perlu dihindari adalah memakai handler sebagai pengganti try-catch. Handler tidak memberi tahu di mana letak kegagalan secara alami, dan program tetap berhenti — pengguna kehilangan hasil kerjanya meskipun handler berjalan mulus. Handler adalah jaring pengaman, bukan strategi utama.

Ringkasan #

set_exception_handler() menerima callable bertipe Throwable yang menjadi tujuan terakhir setiap exception yang lolos dari semua catch. Handler mengambil kendali penuh atas momen terakhir program — mencatat detail kegagalan, menampilkan pesan yang layak, dan membersihkan sisa pekerjaan — tetapi eksekusi tetap berhenti setelah handler selesai, dengan exit code 0. Handler bertumpuk dan bisa dilepas dengan restore_exception_handler(), sementara pemulihan alur yang sesungguhnya tetap menjadi tugas try-catch. Dengan handler terpasang, satu pertanyaan berikutnya muncul alami: ke mana semua catatan terakhir itu sebaiknya disimpan? Chapter tentang logging menjawabnya.

Referensi #