Error database bukan peristiwa luar biasa — ia rutinitas. Pengguna mendaftar dengan email yang sudah terdaftar, kode membaca kolom yang salah ketik, dua proses saling mengunci, koneksi terputus di tengah rilis. Aplikasi yang sehat tidak runtuh oleh peristiwa tersebut; aplikasi menangkap error, membaca klasifikasinya, dan memutuskan reaksi yang tepat: tolak dengan pesan ramah, coba ulang, atau laporkan sebagai bug. Kunci semua keputusan itu adalah satu kode lima karakter bernama SQLSTATE yang dikirim server pada setiap error, dan yang — berbeda dengan teks pesan — dijamin stabil lintas versi maupun driver. Chapter ini membedah anatomi error PostgreSQL, memetakan kode yang paling sering muncul, lalu menangkapnya secara terklasifikasi dari psycopg dan node-postgres.
Anatomi Error PostgreSQL #
Setiap error PostgreSQL membawa paket informasi yang lengkap, bukan sekadar teks pesan. Format verbose psql menampilkan seluruhnya — baris \set VERBOSITY verbose di awal berkas mengaktifkan mode itu sebelum sebuah INSERT duplikat sengaja dibuat gagal:
-- Anatomi error PostgreSQL: aktifkan format verbose psql supaya
-- kode SQLSTATE dan konteks lainnya ikut tampil.
\set VERBOSITY verbose
CREATE TABLE anggota (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nama text NOT NULL,
email text NOT NULL UNIQUE
);
INSERT INTO anggota (nama, email) VALUES ('Budi', '[email protected]');
-- Baris ini gagal karena email sudah dipakai; psql berhenti di sini
-- dan seluruh anatomi error tampil sebagai output berkas ini.
INSERT INTO anggota (nama, email) VALUES ('Budi Lagi', '[email protected]');Output yang Dihasilkan:
psql:anatomi-error.sql:15: ERROR: 23505: duplicate key value violates unique constraint "anggota_email_key"
DETAIL: Key (email)=([email protected]) already exists.
SCHEMA NAME: public
TABLE NAME: anggota
CONSTRAINT NAME: anggota_email_key
LOCATION: _bt_check_unique, nbtinsert.c:666Mari kita baca anatomi yang tampil pada output di atas:
ERROR: 23505:— dua informasi paling penting berdampingan: severity (ERROR) dan kode SQLSTATE (23505, unique violation). Kode inilah yang stabil dan layak dijadikan dasar keputusan program.- Baris
DETAIL:— konteks spesifik kejadian:Key (email)=([email protected]) already existsmenyebut kolom dan nilai yang berkonflik. SCHEMA NAME,TABLE NAME,CONSTRAINT NAME— nama objek tempat error terjadi;anggota_email_keyadalah nama constraint unik yang dilanggar — emas untuk logging karena langsung menunjuk lokasi masalah.LOCATION:— posisi kode di sumber server PostgreSQL yang menimbulkan error; berguna bagi pengembang server, jarang dipakai aplikasi.
Dua pernyataan pertama berkas itu (CREATE TABLE, INSERT 0 1) sukses sebelum error terjadi; output hanya menampilkan error karena psql dengan ON_ERROR_STOP berhenti tepat di baris gagal.
Kode SQLSTATE yang Sering Ditemui #
Daftar lengkapnya hidup di Appendix A dokumentasi PostgreSQL; tabel berikut memuat kode yang paling sering dijumpai aplikasi nyata:
| SQLSTATE | Nama | Pemicu | Reaksi umum |
|---|---|---|---|
23505 | unique_violation | INSERT/UPDATE melanggar unique/PK | Tolak dengan pesan “sudah ada” |
23503 | foreign_key_violation | Referensi ke baris yang tidak ada | Tolak, periksa data induk |
23502 | not_null_violation | Kolom NOT NULL menerima NULL | Tolak, lengkapi input |
23514 | check_violation | Nilai gagal CHECK | Tolak dengan aturan yang dilanggar |
22012 | division_by_zero | Pembagian dengan nol | Bug — perbaiki query |
42P01 | undefined_table | Tabel tidak ada | Bug/skema tidak termigrasi |
42703 | undefined_column | Kolom tidak ada | Bug — salah ketik atau skema telat |
40001 | serialization_failure | Transaksi SERIALIZABLE bentrok | Retry dengan data segar |
40P01 | deadlock_detected | Deadlock, satu transaksi dibunuh server | Retry seluruh transaksi |
55P03 | lock_not_available | NOWAIT/lock_timeout kena lock | Retry atau antre |
08006 | connection_failure | Koneksi gagal/terputus | Retry koneksi dengan backoff |
Exception Terklasifikasi di psycopg #
psycopg 3 menerjemahkan SQLSTATE menjadi class exception bernama di modul psycopg.errors: kode 23505 menjadi errors.UniqueViolation, 23503 menjadi errors.ForeignKeyViolation, dan seterusnya — semuanya turunan psycopg.Error yang menyimpan kode aslinya di atribut .sqlstate. Skrip berikut menyiapkan dua tabel dengan ragam constraint, lalu menjalankan delapan pernyataan: satu subset gagal oleh sebab yang berbeda-beda, dan setiap sebab tertangkap oleh class yang tepat:
# Menangkap error PostgreSQL sebagai exception terklasifikasi di psycopg.
# Setiap class di psycopg.errors punya atribut .sqlstate; .diag membawa
# konteks tambahan (nama constraint, nama kolom) dari server.
import psycopg
from psycopg import errors
conn = psycopg.connect()
cur = conn.cursor()
cur.execute("""
CREATE TABLE pelanggan (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nama text NOT NULL,
email text NOT NULL UNIQUE,
CHECK (length(nama) >= 3)
)
""")
cur.execute("""
CREATE TABLE pesanan (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
pelanggan_id integer NOT NULL REFERENCES pelanggan (id)
)
""")
conn.commit()
def jalankan(label, query, params):
"""Jalankan satu pernyataan dan tangkap errornya per klasifikasi."""
try:
with conn.transaction():
cur.execute(query, params)
print(f"OK : {label}")
except errors.UniqueViolation as e:
print(f"TOLAK : {label} -- email sudah dipakai "
f"(sqlstate {e.sqlstate}, constraint {e.diag.constraint_name})")
except errors.CheckViolation as e:
print(f"TOLAK : {label} -- nama minimal 3 karakter "
f"(sqlstate {e.sqlstate}, constraint {e.diag.constraint_name})")
except errors.NotNullViolation as e:
print(f"TOLAK : {label} -- kolom {e.diag.column_name} wajib diisi "
f"(sqlstate {e.sqlstate})")
except errors.ForeignKeyViolation as e:
print(f"TOLAK : {label} -- pelanggan tidak ditemukan "
f"(sqlstate {e.sqlstate}, constraint {e.diag.constraint_name})")
except errors.UndefinedColumn as e:
print(f"BUG : {label} -- kolom tidak ada "
f"(sqlstate {e.sqlstate})")
except errors.DivisionByZero as e:
print(f"BUG : {label} -- pembagian dengan nol "
f"(sqlstate {e.sqlstate})")
except psycopg.Error as e:
# Kode yang tidak kita antisipasi: catat sqlstate-nya,
# bungkus ulang sebagai error aplikasi.
print(f"ERROR : {label} -- sqlstate {e.sqlstate}")
jalankan("Budi mendaftar",
"INSERT INTO pelanggan (nama, email) VALUES (%s, %s)",
("Budi", "[email protected]"))
jalankan("Budi Duplikat mendaftar",
"INSERT INTO pelanggan (nama, email) VALUES (%s, %s)",
("Budi Duplikat", "[email protected]"))
jalankan("An mendaftar",
"INSERT INTO pelanggan (nama, email) VALUES (%s, %s)",
("An", "[email protected]"))
jalankan("pelanggan tanpa nama",
"INSERT INTO pelanggan (nama, email) VALUES (%s, %s)",
(None, "[email protected]"))
jalankan("pesanan untuk pelanggan 1",
"INSERT INTO pesanan (pelanggan_id) VALUES (%s)", (1,))
jalankan("pesanan untuk pelanggan 99",
"INSERT INTO pesanan (pelanggan_id) VALUES (%s)", (99,))
jalankan("query dengan salah ketik kolom",
"SELECT emel FROM pelanggan", ())
jalankan("query pembagian nol",
"SELECT 1 / jumlah_nol FROM (VALUES (0)) AS t (jumlah_nol)", ())
cur.close()
conn.close()Output yang Dihasilkan:
OK : Budi mendaftar
TOLAK : Budi Duplikat mendaftar -- email sudah dipakai (sqlstate 23505, constraint pelanggan_email_key)
TOLAK : An mendaftar -- nama minimal 3 karakter (sqlstate 23514, constraint pelanggan_nama_check)
TOLAK : pelanggan tanpa nama -- kolom nama wajib diisi (sqlstate 23502)
OK : pesanan untuk pelanggan 1
TOLAK : pesanan untuk pelanggan 99 -- pelanggan tidak ditemukan (sqlstate 23503, constraint pesanan_pelanggan_id_fkey)
BUG : query dengan salah ketik kolom -- kolom tidak ada (sqlstate 42703)
BUG : query pembagian nol -- pembagian dengan nol (sqlstate 22012)Mari kita bedah bagian-bagian penting pola tersebut:
with conn.transaction():di sekitarcur.execute— setiap error membatalkan transaksinya sendiri dan meninggalkan koneksi bersih untuk percobaan berikutnya; tanpa bungkus ini, koneksi tersangkut dalam status aborted setelah error pertama.- Urutan
exceptdari spesifik ke umum —errors.UniqueViolationdiperiksa sebelumpsycopg.Error; class paling umum di akhir berfungsi sebagai jaring pengaman untuk kode yang tidak kita antisipasi. e.sqlstatedane.diag.constraint_name— kode dan nama constraint diambil dari atribut terstruktur, bukan dengan mengurai teks pesan;e.diag.column_namepadaNotNullViolationbahkan menyebut kolom mana yang kosong.- Label reaksi pada output —
TOLAKuntuk pelanggaran data dari pengguna (input ditolak dengan pesan ramah),BUGuntuk kesalahan kode yang harus diperbaiki programmer: klasifikasi yang sama, penanganan yang berbeda.
err.code di node-postgres #
node-postgres tidak memetakan SQLSTATE ke class exception — semua error database datang sebagai Error biasa, dan kode SQLSTATE tersedia di properti err.code (string). Pola penerapannya: bandingkan err.code dengan kode yang diantisipasi, serahkan sisanya ke jalur error umum:
// Klasifikasi error PostgreSQL di node-postgres lewat err.code.
// err.code berisi SQLSTATE (string); err.constraint dan err.detail
// menyertakan konteks dari server.
import pg from "pg";
const client = new pg.Client();
await client.connect();
await client.query(`
CREATE TABLE pelanggan_node (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nama text NOT NULL,
email text NOT NULL UNIQUE
)
`);
await client.query(
"INSERT INTO pelanggan_node (nama, email) VALUES ($1, $2)",
["Budi", "[email protected]"],
);
async function daftarPelanggan(nama, email) {
try {
await client.query(
"INSERT INTO pelanggan_node (nama, email) VALUES ($1, $2)",
[nama, email],
);
console.log(`OK : ${nama} terdaftar`);
} catch (err) {
if (err.code === "23505") {
console.log(`TOLAK : ${nama} gagal -- email ${email} sudah dipakai`);
console.log(` sqlstate=${err.code}, constraint=${err.constraint}`);
} else {
console.log(`ERROR : sqlstate=${err.code} -- ${err.message}`);
}
}
}
await daftarPelanggan("Siti", "[email protected]");
await daftarPelanggan("Budi Lagi", "[email protected]");
await client.end();Output yang Dihasilkan:
OK : Siti terdaftar
TOLAK : Budi Lagi gagal -- email [email protected] sudah dipakai
sqlstate=23505, constraint=pelanggan_node_email_keyOutput memperlihatkan hasil yang setara dengan versi psycopg: pendaftaran Siti berhasil, pendaftaran Budi Lagi ditolak, dan properti err.code berisi 23505 persis seperti e.sqlstate — kode yang sama, dibawa lewat jalur berbeda. Properti err.constraint juga hadir, memuat pelanggan_node_email_key, sehingga pola “klasifikasi dulu, pesan ramah kemudian” tetap bisa diterapkan sama di kedua driver.
Retryable atau Fatal #
Tidak semua error setara: sebagian adalah kondisi sementara yang hilang bila diulang, sebagian adalah kepastian yang akan gagal lagi sebanyak apa pun diulang. Klasifikasi ini menentukan arsitektur penanganan error aplikasi:
| Golongan | SQLSTATE | Sifat | Penanganan |
|---|---|---|---|
| Sementara | 40001, 40P01, 55P03, 08003, 08006 | Bentrok/kebuntuan/putus yang hilang dengan waktu | Retry dengan backoff eksponensial, batasi jumlah percobaan |
| Pelanggaran data | 23505, 23503, 23502, 23514 | Input pengguna melanggar aturan | Tolak, balas dengan pesan ramah tanpa retry |
| Bug aplikasi/skema | 42703, 42P01, 22012 | Akan gagal identik bila diulang | Laporkan sebagai error internal, perbaiki kodenya |
Retry untuk golongan sementara selalu mengulang seluruh transaksi dari awal — bukan hanya pernyataan terakhir — karena hasil kerja transaksi yang gagal sudah tidak bisa dipercaya. Jeda antar percobaan sebaiknya naik eksponensial (misalnya 50 ms, 100 ms, 200 ms) dengan batas atas percobaan, agar aplikasi tidak menghantani server yang sedang sehati. Loop retry lengkapnya — lengkap dengan transaksi yang diulang dan keputusan bisnis yang diambil ulang dari data segar — kita bangun di chapter berikutnya.
Membungkus Error dan Disiplin Logging #
Error database sebaiknya tidak bocor mentah ke lapisan antarmuka: baris TOLAK pada contoh kita adalah bentuk sederhana dari error aplikasi — EmailSudahDipakai, PelangganTidakDitemukan — yang pesannya dikontrol aplikasi, bukan server. Aplikasi berlapis biasanya membungkus psycopg.Error ke dalam exception domain di batas repository, sehingga lapisan atas tidak perlu tahu driver apa yang dipakai di bawah.
Disiplin logging-nya mengikuti prinsip yang sama dengan Chapter SQL Injection
: log yang berguna itu minim. Catat sqlstate, nama constraint, nama tabel, dan ID korrelasi permintaan — jangan menyalin seluruh DETAIL yang berisi nilai milik pengguna, dan jangan menempelkan teks SQL lengkap beserta parameternya ke log.
Satu bagian masih tertinggal dari rantai penanganan error: error seperti 40001 dan 40P01 terjadi di dalam transaksi, dan reaksi yang benar — ROLLBACK, ulangi, ambil keputusan dari data segar — hanya bisa ditulis bila aplikasi memegang kendali penuh atas batas transaksinya. Cara psycopg mengelola BEGIN, COMMIT, ROLLBACK, savepoint, dan loop retry dari kode aplikasi adalah topik penutup section ini, Transaksi dalam Aplikasi.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
anatomi-error.sql
Unduh berkas-- Anatomi error PostgreSQL: aktifkan format verbose psql supaya
-- kode SQLSTATE dan konteks lainnya ikut tampil.
\set VERBOSITY verbose
CREATE TABLE anggota (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nama text NOT NULL,
email text NOT NULL UNIQUE
);
INSERT INTO anggota (nama, email) VALUES ('Budi', '[email protected]');
-- Baris ini gagal karena email sudah dipakai; psql berhenti di sini
-- dan seluruh anatomi error tampil sebagai output berkas ini.
INSERT INTO anggota (nama, email) VALUES ('Budi Lagi', '[email protected]');
tangkap-error-psycopg.py
Unduh berkas# Menangkap error PostgreSQL sebagai exception terklasifikasi di psycopg.
# Setiap class di psycopg.errors punya atribut .sqlstate; .diag membawa
# konteks tambahan (nama constraint, nama kolom) dari server.
import psycopg
from psycopg import errors
conn = psycopg.connect()
cur = conn.cursor()
cur.execute("""
CREATE TABLE pelanggan (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nama text NOT NULL,
email text NOT NULL UNIQUE,
CHECK (length(nama) >= 3)
)
""")
cur.execute("""
CREATE TABLE pesanan (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
pelanggan_id integer NOT NULL REFERENCES pelanggan (id)
)
""")
conn.commit()
def jalankan(label, query, params):
"""Jalankan satu pernyataan dan tangkap errornya per klasifikasi."""
try:
with conn.transaction():
cur.execute(query, params)
print(f"OK : {label}")
except errors.UniqueViolation as e:
print(f"TOLAK : {label} -- email sudah dipakai "
f"(sqlstate {e.sqlstate}, constraint {e.diag.constraint_name})")
except errors.CheckViolation as e:
print(f"TOLAK : {label} -- nama minimal 3 karakter "
f"(sqlstate {e.sqlstate}, constraint {e.diag.constraint_name})")
except errors.NotNullViolation as e:
print(f"TOLAK : {label} -- kolom {e.diag.column_name} wajib diisi "
f"(sqlstate {e.sqlstate})")
except errors.ForeignKeyViolation as e:
print(f"TOLAK : {label} -- pelanggan tidak ditemukan "
f"(sqlstate {e.sqlstate}, constraint {e.diag.constraint_name})")
except errors.UndefinedColumn as e:
print(f"BUG : {label} -- kolom tidak ada "
f"(sqlstate {e.sqlstate})")
except errors.DivisionByZero as e:
print(f"BUG : {label} -- pembagian dengan nol "
f"(sqlstate {e.sqlstate})")
except psycopg.Error as e:
# Kode yang tidak kita antisipasi: catat sqlstate-nya,
# bungkus ulang sebagai error aplikasi.
print(f"ERROR : {label} -- sqlstate {e.sqlstate}")
jalankan("Budi mendaftar",
"INSERT INTO pelanggan (nama, email) VALUES (%s, %s)",
("Budi", "[email protected]"))
jalankan("Budi Duplikat mendaftar",
"INSERT INTO pelanggan (nama, email) VALUES (%s, %s)",
("Budi Duplikat", "[email protected]"))
jalankan("An mendaftar",
"INSERT INTO pelanggan (nama, email) VALUES (%s, %s)",
("An", "[email protected]"))
jalankan("pelanggan tanpa nama",
"INSERT INTO pelanggan (nama, email) VALUES (%s, %s)",
(None, "[email protected]"))
jalankan("pesanan untuk pelanggan 1",
"INSERT INTO pesanan (pelanggan_id) VALUES (%s)", (1,))
jalankan("pesanan untuk pelanggan 99",
"INSERT INTO pesanan (pelanggan_id) VALUES (%s)", (99,))
jalankan("query dengan salah ketik kolom",
"SELECT emel FROM pelanggan", ())
jalankan("query pembagian nol",
"SELECT 1 / jumlah_nol FROM (VALUES (0)) AS t (jumlah_nol)", ())
cur.close()
conn.close()