Kasir Toko Maju Jaya mengetik email yang sudah terdaftar, laporan bulanan membagi angka dengan nol, SELECT INTO STRICT mencari pesanan yang sudah diarsipkan — error di database bukan kejadian langka, melainkan bagian hidup normal aplikasi. Tanpa penanganan, satu error menghentikan seluruh perintah berjalan dan pesannya yang mentah langsung tampil ke pemakai. PL/pgSQL menyediakan blok BEGIN ... EXCEPTION ... END untuk menangkap error di dalam database, memilih jenis error mana yang ditangkap lewat kondisi bernama, membaca kode dan pesannya lewat SQLSTATE serta SQLERRM, bahkan melempar error buatan sendiri dengan RAISE ... USING. Chapter ini mempraktikkan semuanya, termasuk satu perilaku yang sering mengejutkan: blok yang gagal otomatis menggulung balik seluruh perubahannya sendiri.
Menangkap Error dengan EXCEPTION #
Klausa EXCEPTION dipasang setelah badan blok, sebelum END. Ketika sebuah statement di badan blok gagal, PostgreSQL mencocokkan error tersebut dengan daftar WHEN dan menjalankan handler yang cocok — pemanggil function tidak pernah melihat error mentahnya:
-- EXCEPTION WHEN: menangkap error agar pemanggil tetap hidup
CREATE TABLE pelanggan (
id integer PRIMARY KEY,
nama text NOT NULL,
email text NOT NULL UNIQUE
);
INSERT INTO pelanggan (id, nama, email) VALUES
(1, 'Budi Santoso', '[email protected]'),
(2, 'Siti Rahayu', '[email protected]');
CREATE FUNCTION daftar_pelanggan(v_id integer, v_nama text, v_email text)
RETURNS text
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO pelanggan (id, nama, email) VALUES (v_id, v_nama, v_email);
RETURN 'pelanggan baru tersimpan: ' || v_nama;
EXCEPTION WHEN unique_violation THEN
RETURN 'gagal daftar: ' || SQLSTATE || ' - ' || SQLERRM;
END
$$;
SELECT daftar_pelanggan(3, 'Ani Kartika', '[email protected]') AS percobaan_1;
SELECT daftar_pelanggan(4, 'Joko Susilo', '[email protected]') AS percobaan_2;
SELECT id, nama, email FROM pelanggan ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 2
CREATE FUNCTION
percobaan_1
---------------------------------------
pelanggan baru tersimpan: Ani Kartika
(1 row)
percobaan_2
--------------------------------------------------------------------------------------------
gagal daftar: 23505 - duplicate key value violates unique constraint "pelanggan_email_key"
(1 row)
id | nama | email
----+--------------+------------------
1 | Budi Santoso | [email protected]
2 | Siti Rahayu | [email protected]
3 | Ani Kartika | [email protected]
(3 rows)Bedah baris pentingnya:
EXCEPTION WHEN unique_violation THEN ... END— saatINSERTmelanggar constraintUNIQUEpada kolomemail, eksekusi melompat ke handler. Hanya error jenis itu yang ditangkap; error lain tetap lolos ke atas.SQLSTATEdanSQLERRM— dua variabel khusus yang hanya hidup di dalam handler:SQLSTATEberisi kode lima karakter (23505),SQLERRMberisi pesan lengkapnya (duplicate key value violates unique constraint "pelanggan_email_key").- Percobaan kedua mengembalikan teks, bukan error — function tetap “hidup”, sesi tetap jalan, dan pemanggil mendapat jawaban yang bisa ditampilkan dengan sopan. Pemeriksaan tabel terakhir menunjukkan hanya Ani yang benar-benar tersimpan; Joko tidak pernah masuk karena
INSERT-nya dibatalkan.
Tanpa Handler: Eksekusi Berhenti di Error Pertama #
Bandingkan dengan keadaan tanpa penanganan. File berikut menjalankan INSERT yang sama — email [email protected] sudah dipakai Budi — tanpa pembungkus function apa pun:
-- Tanpa handler, error yang sama membatalkan perintah dan menghentikan sesi kerja
INSERT INTO pelanggan (id, nama, email) VALUES (5, 'Dewi Lestari', '[email protected]');Output yang Dihasilkan:
psql:tanpa-handler.sql:2: ERROR: duplicate key value violates unique constraint "pelanggan_email_key"
DETAIL: Key (email)=([email protected]) already exists.Error mentah keluar langsung: duplicate key value violates unique constraint "pelanggan_email_key" beserta DETAIL-nya. Dalam mode ON_ERROR_STOP, psql keluar dengan status error dan sisa perintah file tidak dieksekusi sama sekali. Inilah bedanya: handler mengubah titik berhenti dari “sesi mati di error pertama” menjadi “function memutuskan sendiri apa yang dilaporkan”.
Kondisi Bernama dan Kode SQLSTATE #
Setiap error PostgreSQL membawa SQLSTATE — kode lima karakter yang menjadi patokan penangkapan. Kondisi paling sering ditangkap di function sehari-hari:
| Kondisi bernama | SQLSTATE | Memicu oleh |
|---|---|---|
division_by_zero | 22012 | pembagian nol |
unique_violation | 23505 | pelanggaran PRIMARY KEY / UNIQUE |
no_data_found | P0002 | SELECT INTO STRICT tanpa hasil |
too_many_rows | P0003 | SELECT INTO STRICT dengan banyak hasil |
foreign_key_violation | 23503 | INSERT/UPDATE melanggar foreign key |
check_violation | 23514 | pelanggaran constraint CHECK |
Function berikut memicu empat error pertama secara sengaja lalu menangkap semuanya dengan cabang WHEN yang berbeda-beda:
-- Empat kondisi error bernama yang paling sering ditangkap
CREATE FUNCTION uji_error(skenario text)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_rec record;
BEGIN
IF skenario = 'bagi-nol' THEN
PERFORM 10 / 0;
ELSIF skenario = 'duplikat' THEN
INSERT INTO pelanggan (id, nama, email) VALUES (99, 'Uji', '[email protected]');
ELSIF skenario = 'data-hilang' THEN
SELECT * INTO STRICT v_rec FROM pelanggan WHERE id = 777;
ELSIF skenario = 'baris-berlebih' THEN
SELECT * INTO STRICT v_rec FROM pelanggan;
END IF;
RETURN 'tidak ada error';
EXCEPTION
WHEN division_by_zero THEN RETURN 'tertangkap division_by_zero (SQLSTATE ' || SQLSTATE || ')';
WHEN unique_violation THEN RETURN 'tertangkap unique_violation (SQLSTATE ' || SQLSTATE || ')';
WHEN no_data_found THEN RETURN 'tertangkap no_data_found (SQLSTATE ' || SQLSTATE || ')';
WHEN too_many_rows THEN RETURN 'tertangkap too_many_rows (SQLSTATE ' || SQLSTATE || ')';
END
$$;
SELECT uji_error('bagi-nol') AS uji_1;
SELECT uji_error('duplikat') AS uji_2;
SELECT uji_error('data-hilang') AS uji_3;
SELECT uji_error('baris-berlebih') AS uji_4;Output yang Dihasilkan:
CREATE FUNCTION
uji_1
----------------------------------------------
tertangkap division_by_zero (SQLSTATE 22012)
(1 row)
uji_2
----------------------------------------------
tertangkap unique_violation (SQLSTATE 23505)
(1 row)
uji_3
-------------------------------------------
tertangkap no_data_found (SQLSTATE P0002)
(1 row)
uji_4
-------------------------------------------
tertangkap too_many_rows (SQLSTATE P0003)
(1 row)Daftar WHEN dicocokkan berurutan dari atas, mirip ELSIF: error pertama yang cocok menentukan handler yang berjalan. Perhatikan dua kondisi PL/pgSQL khas: no_data_found dan too_many_rows berkode kelas P — kelas yang khusus diperuntukkan error internal PL/pgSQL, bukan error standar SQL.
Selain kondisi spesifik, ada WHEN OTHERS THEN yang menampung semua error tersisa. Gunakan untuk logging atau pemberian pesan umum, tetapi hati-hati: handler yang menelan error tak terduga menyembunyikan bug. Pola yang aman dicatat, memutuskan, lalu melempar ulang error yang tidak dikenalnya — teknik re-raise di bagian akhir chapter ini.
RAISE: Melempar Error Buatan Sendiri #
RAISE adalah perintah dua peran: mencetak pesan di level ringan (NOTICE, WARNING, DEBUG) dan melempar error sungguhan (RAISE EXCEPTION). Level yang diakui RAISE adalah DEBUG, LOG, INFO, NOTICE, WARNING, dan EXCEPTION. Untuk error, USING membawa atribut tambahan: ERRCODE, DETAIL, dan HINT:
-- RAISE EXCEPTION dengan kode, detail, dan hint buatan sendiri
CREATE FUNCTION cek_stok(v_stok integer)
RETURNS text
LANGUAGE plpgsql
AS $$
BEGIN
IF v_stok < 0 THEN
RAISE EXCEPTION 'stok % tidak boleh negatif', v_stok
USING ERRCODE = 'TMJ01',
DETAIL = 'nilai stok berasal dari penghitungan otomatis',
HINT = 'periksa ulang pengurangan stok saat retur barang';
END IF;
RETURN 'stok valid: ' || v_stok;
END
$$;
CREATE FUNCTION jalur_aman(v_stok integer)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_kode text;
v_pesan text;
v_detail text;
v_hint text;
BEGIN
RETURN cek_stok(v_stok);
EXCEPTION WHEN SQLSTATE 'TMJ01' THEN
GET STACKED DIAGNOSTICS v_kode = RETURNED_SQLSTATE,
v_pesan = MESSAGE_TEXT,
v_detail = PG_EXCEPTION_DETAIL,
v_hint = PG_EXCEPTION_HINT;
RETURN format('ditangkap: kode=%s pesan="%s" detail="%s" hint="%s"',
v_kode, v_pesan, v_detail, v_hint);
END
$$;
SELECT jalur_aman(50) AS aman;
SELECT jalur_aman(-5) AS bermasalah;Output yang Dihasilkan:
CREATE FUNCTION
CREATE FUNCTION
aman
----------------
stok valid: 50
(1 row)
bermasalah
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ditangkap: kode=TMJ01 pesan="stok -5 tidak boleh negatif" detail="nilai stok berasal dari penghitungan otomatis" hint="periksa ulang pengurangan stok saat retur barang"
(1 row)RAISE EXCEPTION 'stok % tidak boleh negatif', v_stok;—%di string pesan digantikan nilai argumen, persis sepertiformat().RAISE EXCEPTIONtanpaUSINGmemakai kode defaultP0001(raise_exception).USING ERRCODE = 'TMJ01'— kode custom lima karakter bebas. Konvensinya dua karakter pertama dipilih di luar kelas standar (mis.TM) supaya tidak bertabrakan dengan kode bawaan PostgreSQL.WHEN SQLSTATE 'TMJ01' THEN— pemanggil menangkap error custom lewat kodenya langsung, karenaTMJ01tidak punya nama bawaan.GET STACKED DIAGNOSTICS ... = PG_EXCEPTION_DETAIL— handler tidak hanya bisa membacaSQLSTATE/SQLERRM; perintah ini mengangkat atribut lengkap error:MESSAGE_TEXT,PG_EXCEPTION_DETAIL,PG_EXCEPTION_HINT,RETURNED_SQLCODE(kode nama sepertiunique_violation), sampai nama tabel dan constraint pelanggar.
Blok yang Gagal Menggulung Balik Dirinya Sendiri #
Perilaku paling penting — dan paling sering disalahpahami — dari EXCEPTION: begitu error terjadi dan ditangkap, seluruh perubahan yang dibuat blok itu sebelumnya dibatalkan. Blok berjalan seperti transaksi mini di dalam function:
-- Perubahan di dalam blok yang gagal digulung balik, perubahan di luar tetap
CREATE TABLE log_pesan (pesan text NOT NULL);
CREATE FUNCTION proses_blok(v_id integer)
RETURNS text
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO log_pesan VALUES ('sebelum blok');
BEGIN
INSERT INTO log_pesan VALUES ('dalam blok');
PERFORM 10 / (v_id - 1);
INSERT INTO log_pesan VALUES ('dalam blok, sukses');
EXCEPTION WHEN division_by_zero THEN
INSERT INTO log_pesan VALUES ('handler: ditelan');
END;
INSERT INTO log_pesan VALUES ('sesudah blok');
RETURN 'selesai';
END
$$;
SELECT proses_blok(1) AS hasil;
SELECT pesan FROM log_pesan ORDER BY pesan;Output yang Dihasilkan:
CREATE TABLE
CREATE FUNCTION
hasil
---------
selesai
(1 row)
pesan
------------------
handler: ditelan
sebelum blok
sesudah blok
(3 rows)Hasilnya tiga baris: sebelum blok, handler: ditelan, dan sesudah blok. Dua baris yang hilang menjelaskan mekanismenya: dalam blok tergulung balik karena berada di badan blok yang gagal, dan dalam blok, sukses tidak pernah tereksekusi. Sementara itu, baris yang ditulis sebelum blok dan sesudah blok tetap hidup — hanya blok bersangkutan yang dikembalikan ke keadaan semula. Di balik layar PostgreSQL membungkus tiap blok EXCEPTION dengan savepoint: error memicu ROLLBACK TO savepoint internal, lalu handler berjalan. Mekanisme savepoint itu sendiri sudah dibahas di chapter Savepoint
.
Blok Bersarang dan Re-raise dengan RAISE; #
Blok BEGIN ... EXCEPTION boleh bersarang, dan perintah RAISE; tanpa argumen melempar ulang error yang sedang ditangani — identitasnya (kode, pesan, detail) terjaga utuh. Pola umumnya: blok dalam mencatat, lalu menyerahkan keputusan ke blok luar:
-- RAISE; melempar ulang error yang sama ke blok luar
CREATE TABLE log_kirim (pesan text NOT NULL);
CREATE FUNCTION kirim_pesanan(v_id integer)
RETURNS text
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO log_kirim VALUES ('mengirim pesanan ' || v_id);
BEGIN
PERFORM 10 / (v_id - 1);
EXCEPTION WHEN division_by_zero THEN
INSERT INTO log_kirim VALUES ('kegagalan dicatat, lempar ulang');
RAISE;
END;
RETURN 'pesanan terkirim';
EXCEPTION WHEN division_by_zero THEN
RETURN 'pengiriman dibatalkan (SQLSTATE ' || SQLSTATE || ')';
END
$$;
SELECT kirim_pesanan(1) AS hasil;
SELECT pesan FROM log_kirim ORDER BY pesan;Output yang Dihasilkan:
CREATE TABLE
CREATE FUNCTION
hasil
----------------------------------------
pengiriman dibatalkan (SQLSTATE 22012)
(1 row)
pesan
-------
(0 rows)Dua hal terjadi sekaligus di contoh ini. Pertama, function mengembalikan pengiriman dibatalkan (SQLSTATE 22012) — error yang di-re-raise blok dalam akhirnya ditangkap blok luar. Kedua, tabel log_kirim kosong total: karena RAISE; membuat error keluar dari blok luar, blok luar juga menggulung balik seluruh perubahannya — termasuk baris mengirim pesanan yang ditulis paling awal dan baris kegagalan dicatat yang ditulis oleh handler blok dalam. Re-raise bukan sekadar meneruskan pesan; ia membatalkan jejak transaksi di semua blok yang dilewatinya.
Function kita kini tangguh terhadap kegagalan: bisa menangkap, melaporkan, dan menggulung balik dengan rapi. Namun ada satu batas yang belum terjangkau: function selalu berjalan di dalam transaksi pemanggilnya — ia tidak boleh COMMIT di tengah jalan. Untuk pekerjaan panjang seperti pengisian ulang stok massal yang ingin menyimpan kemajuan per batch, PostgreSQL menyediakan objek tersendiri: stored procedure dengan CALL. Chapter berikutnya mengupasnya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
blok-rollback.sql
Unduh berkas-- Perubahan di dalam blok yang gagal digulung balik, perubahan di luar tetap
CREATE TABLE log_pesan (pesan text NOT NULL);
CREATE FUNCTION proses_blok(v_id integer)
RETURNS text
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO log_pesan VALUES ('sebelum blok');
BEGIN
INSERT INTO log_pesan VALUES ('dalam blok');
PERFORM 10 / (v_id - 1);
INSERT INTO log_pesan VALUES ('dalam blok, sukses');
EXCEPTION WHEN division_by_zero THEN
INSERT INTO log_pesan VALUES ('handler: ditelan');
END;
INSERT INTO log_pesan VALUES ('sesudah blok');
RETURN 'selesai';
END
$$;
SELECT proses_blok(1) AS hasil;
SELECT pesan FROM log_pesan ORDER BY pesan;
exception-dasar.sql
Unduh berkas-- EXCEPTION WHEN: menangkap error agar pemanggil tetap hidup
CREATE TABLE pelanggan (
id integer PRIMARY KEY,
nama text NOT NULL,
email text NOT NULL UNIQUE
);
INSERT INTO pelanggan (id, nama, email) VALUES
(1, 'Budi Santoso', '[email protected]'),
(2, 'Siti Rahayu', '[email protected]');
CREATE FUNCTION daftar_pelanggan(v_id integer, v_nama text, v_email text)
RETURNS text
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO pelanggan (id, nama, email) VALUES (v_id, v_nama, v_email);
RETURN 'pelanggan baru tersimpan: ' || v_nama;
EXCEPTION WHEN unique_violation THEN
RETURN 'gagal daftar: ' || SQLSTATE || ' - ' || SQLERRM;
END
$$;
SELECT daftar_pelanggan(3, 'Ani Kartika', '[email protected]') AS percobaan_1;
SELECT daftar_pelanggan(4, 'Joko Susilo', '[email protected]') AS percobaan_2;
SELECT id, nama, email FROM pelanggan ORDER BY id;
kondisi-bernama.sql
Unduh berkas-- Empat kondisi error bernama yang paling sering ditangkap
CREATE FUNCTION uji_error(skenario text)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_rec record;
BEGIN
IF skenario = 'bagi-nol' THEN
PERFORM 10 / 0;
ELSIF skenario = 'duplikat' THEN
INSERT INTO pelanggan (id, nama, email) VALUES (99, 'Uji', '[email protected]');
ELSIF skenario = 'data-hilang' THEN
SELECT * INTO STRICT v_rec FROM pelanggan WHERE id = 777;
ELSIF skenario = 'baris-berlebih' THEN
SELECT * INTO STRICT v_rec FROM pelanggan;
END IF;
RETURN 'tidak ada error';
EXCEPTION
WHEN division_by_zero THEN RETURN 'tertangkap division_by_zero (SQLSTATE ' || SQLSTATE || ')';
WHEN unique_violation THEN RETURN 'tertangkap unique_violation (SQLSTATE ' || SQLSTATE || ')';
WHEN no_data_found THEN RETURN 'tertangkap no_data_found (SQLSTATE ' || SQLSTATE || ')';
WHEN too_many_rows THEN RETURN 'tertangkap too_many_rows (SQLSTATE ' || SQLSTATE || ')';
END
$$;
SELECT uji_error('bagi-nol') AS uji_1;
SELECT uji_error('duplikat') AS uji_2;
SELECT uji_error('data-hilang') AS uji_3;
SELECT uji_error('baris-berlebih') AS uji_4;
nested-reraise.sql
Unduh berkas-- RAISE; melempar ulang error yang sama ke blok luar
CREATE TABLE log_kirim (pesan text NOT NULL);
CREATE FUNCTION kirim_pesanan(v_id integer)
RETURNS text
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO log_kirim VALUES ('mengirim pesanan ' || v_id);
BEGIN
PERFORM 10 / (v_id - 1);
EXCEPTION WHEN division_by_zero THEN
INSERT INTO log_kirim VALUES ('kegagalan dicatat, lempar ulang');
RAISE;
END;
RETURN 'pesanan terkirim';
EXCEPTION WHEN division_by_zero THEN
RETURN 'pengiriman dibatalkan (SQLSTATE ' || SQLSTATE || ')';
END
$$;
SELECT kirim_pesanan(1) AS hasil;
SELECT pesan FROM log_kirim ORDER BY pesan;
raise-custom.sql
Unduh berkas-- RAISE EXCEPTION dengan kode, detail, dan hint buatan sendiri
CREATE FUNCTION cek_stok(v_stok integer)
RETURNS text
LANGUAGE plpgsql
AS $$
BEGIN
IF v_stok < 0 THEN
RAISE EXCEPTION 'stok % tidak boleh negatif', v_stok
USING ERRCODE = 'TMJ01',
DETAIL = 'nilai stok berasal dari penghitungan otomatis',
HINT = 'periksa ulang pengurangan stok saat retur barang';
END IF;
RETURN 'stok valid: ' || v_stok;
END
$$;
CREATE FUNCTION jalur_aman(v_stok integer)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_kode text;
v_pesan text;
v_detail text;
v_hint text;
BEGIN
RETURN cek_stok(v_stok);
EXCEPTION WHEN SQLSTATE 'TMJ01' THEN
GET STACKED DIAGNOSTICS v_kode = RETURNED_SQLSTATE,
v_pesan = MESSAGE_TEXT,
v_detail = PG_EXCEPTION_DETAIL,
v_hint = PG_EXCEPTION_HINT;
RETURN format('ditangkap: kode=%s pesan="%s" detail="%s" hint="%s"',
v_kode, v_pesan, v_detail, v_hint);
END
$$;
SELECT jalur_aman(50) AS aman;
SELECT jalur_aman(-5) AS bermasalah;
tanpa-handler.sql
Unduh berkas-- Tanpa handler, error yang sama membatalkan perintah dan menghentikan sesi kerja
INSERT INTO pelanggan (id, nama, email) VALUES (5, 'Dewi Lestari', '[email protected]');