Query bisa gagal karena banyak alasan: email yang duplikat, tabel yang salah ketik nama, koneksi yang terputus, hingga sintaks SQL yang tidak valid. PDO punya tiga mode pelaporan error dan satu exception khusus — PDOException — untuk semua kejadian tersebut, dan memilih mode yang tepat menentukan apakah kegagalan tersebar diam-diam atau tertangani dengan rapi. Pada chapter ini kita akan membedah ketiga mode tersebut, membaca struktur kode SQLSTATE dan errorInfo(), membedakan error koneksi dari error query, lalu menutup dengan dua praktik produksi: membungkus error database ke exception domain dan mencatat kegagalan ke log. Semua contoh sengaja memicu error sungguhan pada SQLite in-memory.
Tiga Mode Error PDO #
Mode error ditentukan lewat atribut PDO::ATTR_ERRMODE dan berlaku untuk seluruh operasi pada koneksi:
| Konstanta | Perilaku saat query gagal |
|---|---|
PDO::ERRMODE_SILENT | tidak ada laporan; method mengembalikan false |
PDO::ERRMODE_WARNING | PHP mencetak Warning, method tetap mengembalikan false |
PDO::ERRMODE_EXCEPTION | PDO melempar PDOException |
Mode ERRMODE_SILENT memindahkan tanggung jawab pemeriksaan ke tangan kita — nilai kembalian false adalah satu-satunya sinyal kegagalan:
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$pdo->exec("CREATE TABLE pengguna (id INTEGER PRIMARY KEY, email TEXT UNIQUE)");
$pdo->exec("INSERT INTO pengguna (email) VALUES ('[email protected]')");
$stmt = $pdo->prepare("INSERT INTO pengguna (email) VALUES (?)");
$hasil = $stmt->execute(['[email protected]']);
echo "SILENT: execute mengembalikan " . var_export($hasil, true) . "\n";Output yang Dihasilkan:
SILENT: execute mengembalikan falseInsert di atas gagal karena kolom email bersifat UNIQUE dan [email protected] sudah ada, tetapi tidak satu pun pesan error muncul — satu-satunya petunjuk adalah false dari execute(). Pada mode ERRMODE_EXCEPTION, kegagalan yang sama berubah menjadi PDOException yang bisa ditangkap:
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE pengguna (id INTEGER PRIMARY KEY, email TEXT UNIQUE)");
$pdo->exec("INSERT INTO pengguna (email) VALUES ('[email protected]')");
$stmt = $pdo->prepare("INSERT INTO pengguna (email) VALUES (?)");
try {
$stmt->execute(['[email protected]']);
echo "baris baru tersimpan\n";
} catch (PDOException $e) {
echo "tertangkap: " . $e->getMessage() . "\n";
echo "SQLSTATE: " . $e->getCode() . "\n";
}Output yang Dihasilkan:
tertangkap: SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: pengguna.email
SQLSTATE: 23000Perilaku Mode WARNING #
Mode ERRMODE_WARNING menempati posisi tengah: PHP mencetak Warning lengkap dengan lokasi berkas, lalu method tetap mengembalikan false. Karena warning mencantumkan path berkas yang bervariasi, contoh berikut memasang set_error_handler() sementara untuk menangkap isi pesannya dalam bentuk yang rapi:
<?php
$warning = null;
set_error_handler(function (int $nomor, string $pesan) use (&$warning): bool {
$warning = $pesan;
return true;
});
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
$pdo->exec("CREATE TABLE pengguna (id INTEGER PRIMARY KEY, email TEXT UNIQUE)");
$pdo->exec("INSERT INTO pengguna (email) VALUES ('[email protected]')");
$hasil = $pdo->exec("INSERT INTO pengguna (email) VALUES ('[email protected]')");
restore_error_handler();
echo "exec mengembalikan " . var_export($hasil, true) . "\n";
echo "warning yang muncul: $warning\n";Output yang Dihasilkan:
exec mengembalikan false
warning yang muncul: PDO::exec(): SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: pengguna.emailDua baris output membuktikan perilaku ganda mode ini: exec() tetap mengembalikan false, dan PHP sempat menampilkan warning berisi SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: pengguna.email sebelum ditangkap handler. Mode ini sesekali berguna saat men-debug kode lama, namun untuk aplikasi modern ERRMODE_EXCEPTION jauh lebih aman — tidak ada jalur eksekusi yang bisa mengabaikan kegagalan.
SQLSTATE dan errorInfo #
Setiap error PDO membawa kode SQLSTATE lima karakter yang distandarisasi, dan detailnya bisa dibaca dari property errorInfo milik PDOException. Kode-kode yang paling sering dijumpai:
| SQLSTATE | Arti | Contoh pemicu |
|---|---|---|
23000 | Integrity constraint violation | duplikat kolom UNIQUE, foreign key gagal |
42S02 | Base table or view not found | query ke tabel yang tidak ada |
42000 | Syntax error or access violation | salah ketik perintah SQL |
HY000 | General error / spesifik driver | galat umum tanpa kelas khusus |
Struktur errorInfo berisi tiga elemen — kode standar, kode internal driver, dan pesan asli driver:
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE pengguna (id INTEGER PRIMARY KEY, email TEXT UNIQUE)");
$pdo->exec("INSERT INTO pengguna (email) VALUES ('[email protected]')");
try {
$pdo->exec("INSERT INTO pengguna (email) VALUES ('[email protected]')");
} catch (PDOException $e) {
echo "SQLSTATE : {$e->errorInfo[0]}\n";
echo "kode driver : {$e->errorInfo[1]}\n";
echo "pesan : {$e->errorInfo[2]}\n";
echo "getCode() : " . $e->getCode() . "\n";
}Output yang Dihasilkan:
SQLSTATE : 23000
kode driver : 19
pesan : UNIQUE constraint failed: pengguna.email
getCode() : 23000errorInfo[0] berisi SQLSTATE 23000 — sama dengan yang dikembalikan getCode(), sehingga cukup memakai salah satunya untuk keputusan program. errorInfo[1] berisi kode internal driver (SQLite memakai 19 untuk pelanggaran constraint), dan errorInfo[2] memuat pesan asli dari database. Karena kode 23000 berlaku sama di SQLite, MySQL, maupun PostgreSQL, percabangan berdasarkan SQLSTATE tetap portabel antar database.
Error Koneksi versus Error Query #
Dua keluarga error ini muncul dari tempat yang berbeda dan menuntut penanganan berbeda. Error koneksi dilempar langsung dari konstruktor new PDO(...) — biasanya karena DSN salah, driver tidak tersedia, atau server tidak bisa dihubungi:
<?php
try {
$pdo = new PDO("sqlite:/direktori/tidak/ada/data.sqlite");
echo "koneksi berhasil\n";
} catch (PDOException $e) {
echo "koneksi gagal: " . $e->getMessage() . "\n";
}
echo "program tetap berjalan\n";Output yang Dihasilkan:
koneksi gagal: SQLSTATE[HY000] [14] unable to open database file
program tetap berjalanContoh di atas sengaja menunjuk berkas SQLite di direktori yang tidak ada; PDO menolak membuatnya dan melempar PDOException berkode HY000. Menangkap exception pada titik koneksi penting karena program jarang bisa melanjutkan tanpa database — pilihan yang wajar adalah menampilkan halaman gagal yang ramah, atau gagal cepat dengan pesan yang jelas di log. Error query, sebaliknya, terjadi setelah koneksi sehat dan umumnya bisa dipulihkan: baris duplikat bisa ditolak dengan pesan validasi, sementara operasi lain tetap berjalan.
Untuk database server, DSN-nya berbeda dan memuat kredensial yang tidak boleh dikeras-kode:
<?php
// Contoh DSN MySQL — butuh server MySQL yang berjalan
$pdo = new PDO(
"mysql:host=localhost;dbname=toko;charset=utf8mb4",
"budi",
"YOUR_DATABASE_PASSWORD"
);Membungkus Error ke Exception Domain #
PDOException menjelaskan kejadian database, bukan aturan bisnis Anda. Ketika kode aplikasi menangkap PDOException di mana-mana, logika bisnis terikat erat pada detail infrastruktur. Praktik yang lebih bersih: repository menerjemahkan error database tertentu menjadi exception domain yang bermakna:
<?php
class EmailSudahDipakai extends RuntimeException
{
}
class PenggunaRepository
{
public function __construct(private PDO $pdo)
{
}
public function simpan(string $email): void
{
$stmt = $this->pdo->prepare("INSERT INTO pengguna (email) VALUES (?)");
try {
$stmt->execute([$email]);
} catch (PDOException $e) {
if ($e->getCode() === "23000") {
throw new EmailSudahDipakai("email $email sudah terdaftar", 0, $e);
}
throw $e;
}
}
}
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE pengguna (id INTEGER PRIMARY KEY, email TEXT UNIQUE)");
$pdo->exec("INSERT INTO pengguna (email) VALUES ('[email protected]')");
$repository = new PenggunaRepository($pdo);
try {
$repository->simpan("[email protected]");
} catch (EmailSudahDipakai $e) {
echo "validasi: {$e->getMessage()}\n";
}
echo "program berlanjut tanpa detail SQL\n";Output yang Dihasilkan:
validasi: email [email protected] sudah terdaftar
program berlanjut tanpa detail SQLTiga keputusan desain bekerja di sini. Pengecekan $e->getCode() === "23000" hanya menerjemahkan pelanggaran integritas — error lain diteruskan apa adanya (throw $e) karena bukan wewenang repository. Exception asli tetap dilampirkan sebagai argumen ketiga ($e), sehingga penyelidikan lebih dalam masih mungkin lewat getPrevious(). Dan kode pemanggil kini berbicara dalam bahasa bisnis — EmailSudahDipakai — tanpa perlu tahu tabel atau SQLSTATE apa pun.
Mencatat Kegagalan ke Log #
Penanganan exception yang baik punya dua sisi: pengguna mendapat pesan ramah, tim pengembang mendapat jejak lengkap untuk penyelidikan. Kegagalan database layak dicatat dengan konteks SQLSTATE dan pesan aslinya:
<?php
function catatErrorDatabase(PDOException $e, string $berkasLog): void
{
$waktu = new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"));
$baris = sprintf("[%s] %s\n", $waktu->format("Y-m-d H:i:s"), $e->getMessage());
file_put_contents($berkasLog, $baris, FILE_APPEND);
}
$berkasLog = "/tmp/php-course-db-error.log";
@unlink($berkasLog);
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE TABLE pengguna (id INTEGER PRIMARY KEY, email TEXT UNIQUE)");
$pdo->exec("INSERT INTO pengguna (email) VALUES ('[email protected]')");
try {
$pdo->exec("INSERT INTO pengguna (email) VALUES ('[email protected]')");
} catch (PDOException $e) {
catatErrorDatabase($e, $berkasLog);
echo "error dicatat, tampilkan pesan ramah ke pengguna\n";
}
echo "isi log:\n" . file_get_contents($berkasLog);
unlink($berkasLog);Output yang Dihasilkan:
error dicatat, tampilkan pesan ramah ke pengguna
isi log:
[2026-01-15 08:00:00] SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: pengguna.emailFungsi catatErrorDatabase() menyusun satu baris log berisi waktu dan pesan exception, lalu menambahkannya ke berkas — pada contoh ini berkas sementara di /tmp yang langsung dihapus setelah dibaca, dan waktu memakai nilai beku agar contoh bisa diverifikasi. Di aplikasi produksi, pola yang sama lebih tepat diarahkan ke error_log() atau library logging seperti Monolog, lengkap dengan trace dan konteks query. Dua hal yang tidak boleh masuk log: kredensial koneksi dan data sensitif pengguna — pesan exception sudah cukup talkative tanpa keduanya.
Ringkasan #
ERRMODE_EXCEPTION adalah mode yang tepat untuk kode modern, getCode() memberi SQLSTATE yang portabel, dan errorInfo melengkapi detail spesifik driver. Error koneksi ditangkap di konstruktor PDO, sedangkan error query diterjemahkan lapisan repository menjadi exception domain yang berbicara bahasa bisnis, dengan jejak lengkap tersimpan di log. Pemahaman tentang cara data masuk dan gagal masuk membuka pintu ke topik keamanan yang paling krusial pada aplikasi database: mencegah SQL injection, yang kita bahas pada chapter berikutnya.