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:

KonstantaPerilaku saat query gagal
PDO::ERRMODE_SILENTtidak ada laporan; method mengembalikan false
PDO::ERRMODE_WARNINGPHP mencetak Warning, method tetap mengembalikan false
PDO::ERRMODE_EXCEPTIONPDO melempar PDOException

Mode ERRMODE_SILENT memindahkan tanggung jawab pemeriksaan ke tangan kita — nilai kembalian false adalah satu-satunya sinyal kegagalan:

php
<?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:

text
SILENT: execute mengembalikan false

Insert 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
<?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:

text
tertangkap: SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: pengguna.email
SQLSTATE: 23000

Perilaku 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
<?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:

text
exec mengembalikan false
warning yang muncul: PDO::exec(): SQLSTATE[23000]: Integrity constraint violation: 19 UNIQUE constraint failed: pengguna.email

Dua 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:

SQLSTATEArtiContoh pemicu
23000Integrity constraint violationduplikat kolom UNIQUE, foreign key gagal
42S02Base table or view not foundquery ke tabel yang tidak ada
42000Syntax error or access violationsalah ketik perintah SQL
HY000General error / spesifik drivergalat umum tanpa kelas khusus

Struktur errorInfo berisi tiga elemen — kode standar, kode internal driver, dan pesan asli driver:

php
<?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:

text
SQLSTATE    : 23000
kode driver : 19
pesan       : UNIQUE constraint failed: pengguna.email
getCode()   : 23000

errorInfo[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
<?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:

text
koneksi gagal: SQLSTATE[HY000] [14] unable to open database file
program tetap berjalan

Contoh 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
<?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
<?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:

text
validasi: email [email protected] sudah terdaftar
program berlanjut tanpa detail SQL

Tiga 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
<?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:

text
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.email

Fungsi 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.

Referensi #