Error Handling — PHP

PHP menggunakan Exception untuk menangani error secara terstruktur. Try/Catch: try { $result = riskyOperation(); echo $result; } catch (Exception $e) { echo…

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";

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

Urutan try, catch, finally

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

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.

Yang akan kamu pelajari