Eksepsi #
Eksepsi adalah mekanisme yang memungkinkan kode untuk memberi sinyal bahwa sesuatu yang tidak diharapkan telah terjadi — dan memindahkan tanggung jawab penanganannya ke pemanggil yang lebih tahu konteks situasinya. Tanpa eksepsi, setiap fungsi harus mengembalikan kode error dan setiap pemanggil harus memeriksa nilai kembalian, menciptakan kode yang dipenuhi pengecekan berulang yang mengaburkan logika utama. PHP memiliki dua hierarki error handling: Exception untuk kondisi yang bisa dipulihkan dan Error untuk kegagalan internal PHP yang biasanya tidak bisa dipulihkan. Mengetahui perbedaannya, kapan harus melempar eksepsi vs mengembalikan null, bagaimana merancang hierarki eksepsi kustom yang bermakna, dan bagaimana finally bekerja sebagai resource cleanup — semua ini adalah yang membedakan penanganan error yang baik dari yang sekadar tidak crash.
Hierarki Throwable PHP #
Sejak PHP 7, semua yang bisa di-throw mengimplementasikan interface Throwable. Ada dua cabang utama:
flowchart TD
T[Throwable\ninterface] --> E[Exception]
T --> Err[Error]
E --> RE[RuntimeException]
E --> LA[LogicException]
E --> OE[OverflowException]
E --> UE[UnexpectedValueException]
E --> IO[InvalidArgumentException]
E --> BO[BadMethodCallException]
Err --> TE[TypeError]
Err --> PE[ParseError]
Err --> AE[ArithmeticError]
Err --> VE[ValueError]
RE --> OOR[OutOfRangeException]
RE --> OOB[OutOfBoundsException]
RE --> UOE[UnderflowException]
style T fill:#fef9c3,stroke:#ca8a04
style E fill:#dcfce7,stroke:#16a34a
style Err fill:#fee2e2,stroke:#dc2626Exception dan turunannya digunakan untuk kondisi yang bisa diketahui dan ditangani oleh aplikasi — validasi gagal, resource tidak ditemukan, permission ditolak. Error dan turunannya merepresentasikan kegagalan internal PHP seperti TypeError (argumen tipe salah), ParseError (sintaks file buruk), atau ArithmeticError (pembagian nol pada integer). Kamu hampir tidak pernah perlu catch (Error $e) kecuali untuk logging tingkat tertinggi.
Sintaks Dasar: try, catch, finally
#
<?php
try {
// Kode yang mungkin melempar eksepsi ditempatkan di sini
$hasil = bagi(10, 0);
echo "Hasil: $hasil"; // Tidak pernah tercapai jika bagi() throw
} catch (\InvalidArgumentException $e) {
// Tangkap tipe eksepsi spesifik — lebih diprioritaskan
echo "Argumen tidak valid: " . $e->getMessage();
} catch (\RuntimeException $e) {
// Tangkap tipe yang lebih luas
echo "Error runtime: " . $e->getMessage();
} catch (\Exception $e) {
// Tangkap semua Exception yang belum tertangkap di atas
echo "Error tak terduga: " . $e->getMessage();
} finally {
// Selalu dijalankan — baik ada eksepsi maupun tidak
echo "\nProses selesai.";
}
Cara Kerja Alur Eksepsi #
sequenceDiagram
participant Pemanggil
participant try_block as Blok try
participant catch_block as Blok catch
participant finally_block as Blok finally
Pemanggil->>try_block: Eksekusi kode
alt Tidak ada eksepsi
try_block-->>finally_block: Lanjut ke finally
finally_block-->>Pemanggil: Return normal
else Eksepsi dilempar
try_block->>catch_block: Eksepsi diteruskan
catch_block-->>finally_block: Setelah catch selesai
finally_block-->>Pemanggil: Return setelah handling
else Eksepsi tidak tertangkap
try_block->>finally_block: finally tetap jalan
finally_block->>Pemanggil: Eksepsi propagates up
endfinally — Selalu Dijalankan
#
finally dieksekusi dalam semua kondisi: ketika kode berhasil, ketika eksepsi ditangkap, bahkan ketika eksepsi tidak tertangkap dan sedang propagating ke atas. Ini menjadikannya tempat yang tepat untuk cleanup resource:
<?php
function bacaFile(string $path): string
{
$handle = fopen($path, 'r');
if ($handle === false) {
throw new \RuntimeException("Gagal membuka file: $path");
}
try {
$isi = fread($handle, filesize($path));
if ($isi === false) {
throw new \RuntimeException("Gagal membaca file: $path");
}
return $isi;
} finally {
// Selalu menutup file handle — meski ada eksepsi atau return di atas
fclose($handle);
// Tidak perlu return di sini — nilai return dari try tetap dikembalikan
}
}
// File handle SELALU ditutup, apapun yang terjadi
try {
$isi = bacaFile('/etc/hosts');
echo strlen($isi) . " bytes dibaca";
} catch (\RuntimeException $e) {
echo "Error: " . $e->getMessage();
}
returndi dalam blokfinallyakan menggantikan return value dari bloktryataucatch. Ini adalah perilaku yang tidak intuitif dan bisa menyembunyikan nilai yang dimaksud dikembalikan. Gunakanfinallyhanya untuk side effect (tutup koneksi, hapus file temp, log) — jangan pernahreturndarifinally.
Properti Objek Exception #
Setiap objek Exception membawa informasi yang berguna untuk debugging:
<?php
try {
throw new \RuntimeException("Koneksi database gagal", 500);
} catch (\RuntimeException $e) {
echo $e->getMessage(); // "Koneksi database gagal"
echo $e->getCode(); // 500 — kode error opsional
echo $e->getFile(); // "/var/www/app/Database.php"
echo $e->getLine(); // 42 — baris tempat throw
echo $e->getTraceAsString(); // Stack trace lengkap sebagai string
// Stack trace sebagai array (lebih mudah diproses)
$trace = $e->getTrace();
foreach ($trace as $frame) {
echo "{$frame['file']}:{$frame['line']} — {$frame['function']}()\n";
}
}
Exception Chaining — Menyimpan Penyebab Asli #
Saat menangkap eksepsi tingkat rendah dan melempar eksepsi tingkat tinggi, selalu sertakan eksepsi asli sebagai previous exception agar stack trace tidak hilang:
<?php
class OrderException extends \RuntimeException {}
function simpanOrder(array $data): int
{
try {
$stmt = $pdo->prepare("INSERT INTO orders ...");
$stmt->execute($data);
return (int) $pdo->lastInsertId();
} catch (\PDOException $e) {
// ANTI-PATTERN: exception asli hilang, debug jadi susah
throw new OrderException("Gagal menyimpan order");
// BENAR: sertakan $e sebagai previous exception (argumen ketiga)
throw new OrderException("Gagal menyimpan order", 0, $e);
}
}
try {
simpanOrder($data);
} catch (OrderException $e) {
echo $e->getMessage(); // "Gagal menyimpan order"
echo $e->getPrevious()->getMessage(); // "SQLSTATE[...]: ..." — detail asli
}
Membuat Eksepsi Kustom yang Bermakna #
Exception bawaan PHP (RuntimeException, InvalidArgumentException, dll.) sering cukup untuk kode sederhana. Tapi untuk aplikasi yang lebih besar, eksepsi kustom yang spesifik membuat penanganan error jauh lebih mudah dan pesan error jauh lebih informatif.
Hierarki Eksepsi Domain #
Rancang hierarki eksepsi yang mencerminkan domain aplikasimu:
<?php
// Base exception untuk seluruh domain aplikasi
class AppException extends \RuntimeException {}
// Eksepsi spesifik per domain — extend AppException
class NotFoundException extends AppException
{
public function __construct(string $resource, int|string $id)
{
parent::__construct(
"$resource dengan ID '$id' tidak ditemukan",
404
);
}
}
class ValidationException extends AppException
{
private array $errors;
public function __construct(array $errors)
{
$this->errors = $errors;
parent::__construct(
"Validasi gagal: " . implode(', ', array_keys($errors)),
422
);
}
public function getErrors(): array
{
return $this->errors;
}
public function getFirstError(): string
{
return reset($this->errors) ?: '';
}
}
class AuthorizationException extends AppException
{
public function __construct(string $aksi, string $resource = '')
{
$pesan = $resource
? "Tidak diizinkan untuk '$aksi' pada '$resource'"
: "Tidak diizinkan untuk '$aksi'";
parent::__construct($pesan, 403);
}
}
class DomainException extends AppException {}
// Sub-domain eksepsi yang lebih spesifik
class InsufficientStockException extends DomainException
{
public function __construct(
private string $namaProduk,
private int $diminta,
private int $tersedia,
) {
parent::__construct(
"Stok '$namaProduk' tidak mencukupi: diminta $diminta, tersedia $tersedia",
409
);
}
public function getNamaProduk(): string { return $this->namaProduk; }
public function getDiminta(): int { return $this->diminta; }
public function getTersedia(): int { return $this->tersedia; }
}
// Penggunaan
function kurangiStok(int $produkId, int $jumlah): void
{
$produk = cariProduk($produkId);
if ($produk === null) {
throw new NotFoundException('Produk', $produkId);
}
if ($jumlah > $produk['stok']) {
throw new InsufficientStockException(
$produk['nama'],
$jumlah,
$produk['stok']
);
}
// kurangi stok...
}
Tangkap Hierarki Eksepsi secara Berlapis #
Dengan hierarki yang dirancang dengan baik, kamu bisa menangkap di level yang tepat:
<?php
try {
kurangiStok(42, 100);
} catch (InsufficientStockException $e) {
// Level paling spesifik — bisa akses detail
echo "Stok kurang: " . $e->getNamaProduk();
echo " Diminta: " . $e->getDiminta();
echo " Tersedia: " . $e->getTersedia();
// Tampilkan form untuk ubah jumlah
} catch (NotFoundException $e) {
// Lebih luas — hanya tahu resource tidak ada
http_response_code(404);
echo $e->getMessage();
} catch (AppException $e) {
// Tangkap semua eksepsi domain — respons umum
http_response_code($e->getCode() ?: 500);
echo "Error: " . $e->getMessage();
} catch (\Throwable $e) {
// Paling luas — tangkap semua termasuk Error (TypeError, dll.)
// Hanya untuk logging — jangan expose detail ke user
error_log($e->getMessage() . "\n" . $e->getTraceAsString());
http_response_code(500);
echo "Terjadi kesalahan internal";
}
Multi-Catch — Menangkap Beberapa Tipe Sekaligus #
Sejak PHP 8.0, beberapa tipe eksepsi bisa ditangkap dalam satu blok catch dengan operator |:
<?php
try {
$hasil = prosesInput($input);
} catch (ValidationException | \InvalidArgumentException $e) {
// Tangkap keduanya dengan penanganan yang sama
http_response_code(422);
echo "Input tidak valid: " . $e->getMessage();
} catch (NotFoundException | \OutOfBoundsException $e) {
http_response_code(404);
echo "Data tidak ditemukan";
} catch (\PDOException | \RuntimeException $e) {
// Database atau runtime error — log dan tampilkan pesan umum
error_log($e->getTraceAsString());
http_response_code(500);
echo "Terjadi kesalahan server";
}
Re-throw — Melempar Ulang Eksepsi #
Terkadang kamu perlu menangkap eksepsi untuk melakukan sesuatu (logging, cleanup) tapi tetap ingin eksepsi itu propagate ke atas:
<?php
function eksekusiTransaksi(callable $operasi): mixed
{
$pdo->beginTransaction();
try {
$hasil = $operasi($pdo);
$pdo->commit();
return $hasil;
} catch (\Throwable $e) {
// Rollback dulu sebelum eksepsi di-re-throw
$pdo->rollBack();
// Log detail teknis (tidak ditampilkan ke user)
error_log("Transaksi gagal: " . $e->getMessage());
error_log($e->getTraceAsString());
// Re-throw — biarkan pemanggil yang memutuskan cara menanganinya
throw $e;
// Atau wrap dengan eksepsi yang lebih deskriptif:
// throw new TransactionException("Transaksi gagal", 0, $e);
}
}
// Pemanggil mendapat eksepsi asli (atau wrapper-nya)
try {
$orderId = eksekusiTransaksi(function(\PDO $db) use ($data) {
$id = simpanOrder($db, $data);
kurangiStok($db, $data['produk_id'], $data['qty']);
return $id;
});
} catch (InsufficientStockException $e) {
echo "Stok habis: " . $e->getNamaProduk();
} catch (\PDOException $e) {
echo "Error database";
}
throw sebagai Ekspresi (PHP 8.0+)
#
Sejak PHP 8.0, throw adalah ekspresi — bisa digunakan di dalam ternary, null coalescing, dan arrow function:
<?php
// throw dalam null coalescing
$nama = $_GET['nama'] ?? throw new \InvalidArgumentException("Parameter 'nama' wajib ada");
// throw dalam ternary
$umur = is_numeric($_GET['umur'] ?? '')
? (int) $_GET['umur']
: throw new \InvalidArgumentException("Umur harus berupa angka");
// throw dalam arrow function
$parse = fn(string $json) => json_decode($json, true)
?? throw new \ValueError("JSON tidak valid: $json");
// throw di dalam match
$status = match($kode) {
200 => 'ok',
404 => 'not_found',
default => throw new \UnexpectedValueException("Kode HTTP tidak dikenal: $kode"),
};
// throw sebagai short-circuit guard
function cariUserOrFail(int $id): array
{
return cariUser($id)
?? throw new NotFoundException('User', $id);
}
Global Exception Handler #
Untuk eksepsi yang tidak tertangkap oleh blok try/catch manapun, PHP menyediakan mekanisme global handler. Ini penting untuk logging dan menampilkan halaman error yang ramah pengguna:
<?php
// set_exception_handler — untuk Exception yang tidak tertangkap
set_exception_handler(function (\Throwable $e): void {
// Log detail teknis
$pesan = sprintf(
"[%s] %s di %s:%d\nStack trace:\n%s",
get_class($e),
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString()
);
error_log($pesan);
// Respons yang tepat berdasarkan konteks
if (php_sapi_name() === 'cli') {
fwrite(STDERR, "Fatal: " . $e->getMessage() . "\n");
exit(1);
}
// Untuk web — tampilkan halaman error yang bersih
$kode = $e instanceof AppException ? $e->getCode() : 500;
http_response_code($kode ?: 500);
if (getenv('APP_ENV') === 'development') {
// Di development — tampilkan detail untuk debugging
echo "<pre>" . htmlspecialchars($pesan) . "</pre>";
} else {
// Di production — tampilkan halaman error generik
echo "Terjadi kesalahan. Silakan coba lagi nanti.";
}
});
// set_error_handler — konversi PHP error tradisional ke Exception
set_error_handler(function(int $errno, string $errstr, string $errfile, int $errline): bool {
if (!(error_reporting() & $errno)) {
return false; // Error ini diabaikan oleh error_reporting setting
}
throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
});
Exception vs Kode Error — Kapan Mana #
Tidak semua situasi abnormal harus menggunakan eksepsi. Panduan ini membantu memilih:
Gunakan Exception jika:
✓ Kondisi yang benar-benar exceptional — jarang terjadi dalam alur normal
✓ Kegagalan yang pemanggil tidak bisa lanjut tanpanya (file tidak ada, DB down)
✓ Kontrak fungsi dilanggar (argumen invalid, state tidak konsisten)
✓ Kegagalan yang perlu propagate beberapa level ke atas
Jangan gunakan Exception jika:
✗ Kondisi yang sering terjadi dalam alur normal (user tidak login, form kosong)
✗ Validasi yang diharapkan gagal (input user salah format)
✗ Pencarian yang hasilnya "tidak ditemukan" adalah kasus valid
✗ Untuk flow control (seperti break dari loop)
<?php
// ANTI-PATTERN: Exception untuk flow control normal
function cariUserByEmail(string $email): array
{
$user = queryDatabase($email);
if (!$user) {
throw new NotFoundException('User', $email); // email tidak ada itu normal!
}
return $user;
}
// Caller terpaksa pakai try/catch untuk flow biasa:
try {
$user = cariUserByEmail($email);
// user ada
} catch (NotFoundException $e) {
// user tidak ada — ini alur normal, bukan exceptional!
}
// BENAR: kembalikan null untuk "tidak ditemukan" yang valid
function cariUserByEmail(string $email): ?array
{
return queryDatabase($email) ?: null;
}
// Caller menggunakan null check biasa:
$user = cariUserByEmail($email);
if ($user === null) {
// user tidak ada
}
// Exception tetap tepat untuk: argumen yang jelas invalid
function cariUserById(int $id): ?array
{
if ($id <= 0) {
throw new \InvalidArgumentException("ID harus positif, diterima: $id");
}
return queryDatabase($id) ?: null;
}
Pola Result Type — Alternatif Eksepsi #
Untuk operasi yang bisa gagal dengan cara yang dapat diprediksi, pola Result type (dipinjam dari functional programming) bisa lebih ekspresif dari eksepsi:
<?php
class Result
{
private function __construct(
private readonly bool $sukses,
private readonly mixed $nilai,
private readonly string $error = '',
) {}
public static function ok(mixed $nilai): static
{
return new static(true, $nilai);
}
public static function gagal(string $error): static
{
return new static(false, null, $error);
}
public function sukses(): bool { return $this->sukses; }
public function gagal(): bool { return !$this->sukses; }
public function nilai(): mixed { return $this->nilai; }
public function error(): string { return $this->error; }
}
// Operasi yang mengembalikan Result
function validasiEmail(string $email): Result
{
if (empty($email)) {
return Result::gagal("Email tidak boleh kosong");
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return Result::gagal("Format email tidak valid: $email");
}
return Result::ok($email);
}
function daftarUser(string $nama, string $email): Result
{
$validasi = validasiEmail($email);
if ($validasi->gagal()) {
return Result::gagal($validasi->error());
}
// Lanjut proses...
return Result::ok(['id' => 1, 'nama' => $nama, 'email' => $email]);
}
// Pemanggil tidak perlu try/catch
$result = daftarUser("Budi", "budi-tidak-valid");
if ($result->gagal()) {
echo "Gagal: " . $result->error();
} else {
$user = $result->nilai();
echo "Berhasil: user #{$user['id']} dibuat";
}
Anti-Pattern Eksepsi yang Sering Ditemui #
<?php
// ✗ Anti-pattern 1: catch semua dan abaikan
try {
prosesKompleks();
} catch (\Exception $e) {
// Diam-diam mengabaikan error — sangat berbahaya!
// Bug tersembunyi dan tidak ada jejak yang bisa di-debug
}
// ✓ Minimal log sebelum lanjut
try {
prosesKompleks();
} catch (\Exception $e) {
error_log("Proses gagal: " . $e->getMessage());
// Lanjutkan dengan nilai default atau re-throw
}
// ✗ Anti-pattern 2: pesan error yang tidak informatif
throw new \Exception("Error"); // error apa?
throw new \Exception("Terjadi masalah"); // masalah apa?
// ✓ Pesan yang spesifik dan actionable
throw new \RuntimeException(
"Gagal mengirim email ke '{$email}': SMTP server tidak merespons (timeout 30s)"
);
// ✗ Anti-pattern 3: catch terlalu luas di tempat yang salah
function hitungTotal(array $items): float
{
try {
return array_sum(array_column($items, 'harga'));
} catch (\Exception $e) {
return 0; // Sembunyikan error — fungsi ini tidak perlu try/catch sama sekali!
}
}
// ✓ Biarkan eksepsi propagate — tangkap di tempat yang tepat (pemanggil)
function hitungTotal(array $items): float
{
return array_sum(array_column($items, 'harga'));
}
// ✗ Anti-pattern 4: gunakan Exception untuk validasi input user biasa
function prosesFormLogin(string $email, string $password): array
{
if (empty($email)) throw new \Exception("Email kosong");
if (empty($password)) throw new \Exception("Password kosong");
// ...
}
// ✓ Validasi biasa tidak perlu Exception — kembalikan error array
function prosesFormLogin(string $email, string $password): array
{
$errors = [];
if (empty($email)) $errors['email'] = "Email tidak boleh kosong";
if (empty($password)) $errors['password'] = "Password tidak boleh kosong";
if (!empty($errors)) {
return ['sukses' => false, 'errors' => $errors];
}
// proses login...
return ['sukses' => true, 'user' => $user];
}
Ringkasan #
- Hierarki PHP:
Exceptionuntuk kondisi yang bisa dipulihkan aplikasi;Erroruntuk kegagalan internal PHP (TypeError, ParseError). Tangkap\Throwablehanya di global handler untuk logging.finallyselalu dijalankan — baik ada eksepsi maupun tidak. Gunakan untuk resource cleanup (tutup file, rollback transaksi) tapi janganreturndarifinallykarena akan menggantikan return value daritry.- Exception chaining — saat re-throw, sertakan eksepsi asli sebagai argumen ketiga
new MyException("...", 0, $e). GunakangetPrevious()untuk mengakses penyebab aslinya.- Eksepsi kustom yang informatif membawa data spesifik domain (
getErrors(),getNamaProduk(),getDiminta()) sehingga pemanggil bisa merespons dengan tepat tanpa harus mem-parse string pesan.- Multi-catch
catch (TypeA | TypeB $e)memungkinkan menangani beberapa tipe eksepsi dengan kode yang sama tanpa duplikasi blokcatch.throwsebagai ekspresi (PHP 8.0+) memungkinkan lempar eksepsi dalam null coalescing??, ternary,match, dan arrow function — membuat guard condition lebih ringkas.- Exception untuk kondisi exceptional — bukan untuk alur normal seperti “tidak ditemukan” atau “validasi form gagal”. Untuk kasus tersebut, kembalikan
null, array errors, atau gunakan pola Result.- Global handler via
set_exception_handler()danset_error_handler()memastikan semua eksepsi yang tidak tertangkap dilog dan menghasilkan respons yang sesuai, bukan halaman error PHP default yang mengekspos informasi sensitif.