Error di SQL tidak datang sendirian: duplikat kunci primary, kolom wajib yang terisi NULL, tabel yang tidak ada. Di luar stored program, error cukup diterima pemanggil — selesai. Di dalam prosedur yang menjalankan sepuluh statement berurutan, satu error di tengah menghentikan sembilan statement sisanya tanpa pesan apa pun, dan pemanggil hanya melihat baris error mentah. Handler mengubah situasi ini: blok penangkap yang menentukan apa yang terjadi ketika error menyala — lanjut kerja, keluar dengan sopan, catat pesannya, atau malah melempar error buatan sendiri. Pada chapter ini kita membedah DECLARE ... HANDLER, perbedaan CONTINUE dan EXIT, pembacaan pesan lewat GET DIAGNOSTICS, pembuatan error kustom dengan SIGNAL, dan penerusannya lewat RESIGNAL.

Menyiapkan Database Latihan #

Kita bangun ulang database belajar_sp beserta tabel produk, ditambah tabel log_error untuk latihan pencatatan kegagalan:

sql
DROP DATABASE IF EXISTS belajar_sp;
CREATE DATABASE belajar_sp;
USE belajar_sp;

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Database changed
sql
CREATE TABLE produk (
  id INT PRIMARY KEY,
  nama VARCHAR(50) NOT NULL,
  harga DECIMAL(10,2) NOT NULL,
  stok INT NOT NULL
);

INSERT INTO produk (id, nama, harga, stok) VALUES
  (1, 'Kopi Gayo', 55000.00, 10),
  (2, 'Teh Melati', 25000.00, 20),
  (3, 'Gula Aren', 15000.00, 30),
  (4, 'Susu Kental', 8000.00, 40),
  (5, 'Kopi Toraja', 62000.00, 5);

CREATE TABLE log_error (
  id INT AUTO_INCREMENT PRIMARY KEY,
  pesan VARCHAR(200) NOT NULL
);

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 5 rows affected (0.00 sec)
Records: 5  Duplicates: 0  Warnings: 0

Query OK, 0 rows affected (0.00 sec)

Tiga Kelas Kondisi dan Bentuk Handler #

Setiap error MySQL membawa SQLSTATE — kode lima digit yang mengelompokkan penyebabnya. Dua digit pertama menentukan kelasnya, dan handler bisa menangkap berdasarkan kelas tersebut:

KelasSQLSTATE diawaliMaknaNama ringkas
Sukses00berhasil—
Peringatan01berhasil dengan catatanSQLWARNING
Data kosong02query tidak menemukan barisNOT FOUND
Errorselain itukegagalanSQLEXCEPTION

Handler dideklarasikan dengan pola DECLARE jenis HANDLER FOR kondisi aksi;. kondisi boleh berupa nama kelas (SQLEXCEPTION, NOT FOUND), SQLSTATE eksplisit (SQLSTATE '42S02'), atau kode error numerik MySQL (1062). jenis menentukan kelanjutan eksekusi — dan di situlah perbedaan besar pertama yang kita uji.

CONTINUE Handler: Menangkap lalu Melanjutkan #

Prosedur demo_continue mencoba menyisipkan produk dengan id yang sudah dipakai. Handler CONTINUE untuk kode error 1062 (entri duplikat) menangkapnya, mencetak laporan, lalu membiarkan statement berikutnya tetap berjalan:

sql
DROP PROCEDURE IF EXISTS demo_continue;
DELIMITER $$
CREATE PROCEDURE demo_continue()
BEGIN
  DECLARE CONTINUE HANDLER FOR 1062
    SELECT 'duplikat diabaikan' AS info;

  INSERT INTO produk (id, nama, harga, stok) VALUES
    (1, 'Kopi Gayo Duplikat', 99000.00, 1);
  SELECT COUNT(*) AS jumlah_produk_setelah;
END$$
DELIMITER ;
CALL demo_continue();

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+--------------------+
| info               |
+--------------------+
| duplikat diabaikan |
+--------------------+
1 row in set (0.00 sec)

+-----------------------+
| jumlah_produk_setelah |
+-----------------------+
|                     1 |
+-----------------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Dua result set kembali dari satu CALL: laporan handler bahwa duplikat diabaikan, dan jumlah produk yang tetap 5 — INSERT gagal, namun statement SELECT COUNT(*) sesudahnya tetap dieksekusi. Jenis CONTINUE cocok untuk kegagalan yang memang bisa dilewati: baris duplikat pada data impor, nilai opsional yang hilang.

EXIT Handler: Menangkap lalu Keluar #

EXIT memperlakukan error sebagai alasan berhenti: handler berjalan, lalu eksekusi meninggalkan blok BEGIN ... END tempat handler dideklarasikan — statement sisa blok itu dilewati:

sql
DROP PROCEDURE IF EXISTS demo_exit;
DELIMITER $$
CREATE PROCEDURE demo_exit()
BEGIN
  DECLARE EXIT HANDLER FOR 1062
    SELECT 'duplikat menghentikan prosedur' AS info;

  INSERT INTO produk (id, nama, harga, stok) VALUES
    (1, 'Kopi Gayo Duplikat', 99000.00, 1);
  SELECT 'baris ini tidak pernah tampil' AS info;
END$$
DELIMITER ;
CALL demo_exit();

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+--------------------------------+
| info                           |
+--------------------------------+
| duplikat menghentikan prosedur |
+--------------------------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Hanya satu result set yang muncul: laporan handler. Statement SELECT 'baris ini tidak pernah tampil' dilewati diam-diam karena EXIT sudah membawa eksekusi keluar sebelum statement itu sempat berjalan. Pilihan CONTINUE atau EXIT adalah keputusan desain: kalau statement berikutnya bergantung pada keberhasilan statement yang gagal (insert lalu baca hasilnya), EXIT adalah satu-satunya pilihan yang masuk akal.

GET DIAGNOSTICS: Membaca Pesan Error yang Tertangkap #

Laporan generik seperti “duplikat diabaikan” sering kurang — kita ingin tahu error aslinya. Statement GET DIAGNOSTICS CONDITION 1 membaca informasi kondisi yang barusan tertangkap, termasuk MESSAGE_TEXT-nya:

sql
DROP PROCEDURE IF EXISTS diagnosa_error;
DELIMITER $$
CREATE PROCEDURE diagnosa_error()
BEGIN
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    GET DIAGNOSTICS CONDITION 1 @pesan = MESSAGE_TEXT;
    SELECT @pesan AS pesan_error;
  END;

  INSERT INTO produk (id, nama, harga, stok) VALUES
    (1, 'Duplikat', 1.00, 1);
  SELECT 'tidak tercapai' AS info;
END$$
DELIMITER ;
CALL diagnosa_error();

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+----------------------------------------------+
| pesan_error                                  |
+----------------------------------------------+
| Duplicate entry '1' for key 'produk.PRIMARY' |
+----------------------------------------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Handler di sini menangkap kelas SQLEXCEPTION — semua SQLSTATE di luar peringatan dan data-kosong — sehingga duplikat 1062 pun ikut tertangkap. Isi handler-nya kini blok BEGIN ... END kecil dengan dua statement: GET DIAGNOSTICS CONDITION 1 @pesan = MESSAGE_TEXT menyalin teks pesan kondisi pertama ke variabel sesi @pesan, lalu SELECT menampilkannya. Hasilnya pesan asli dari server — Duplicate entry '1' for key 'produk.PRIMARY' — lengkap dengan nama kunci yang dilanggar, informasi yang jauh lebih berharga untuk log dibanding laporan generik buatan kita sendiri. CONDITION 1 merujuk kondisi terbaru; bila satu statement memicu beberapa kondisi sekaligus, kondisi kedua dan seterusnya bisa dibaca lewat CONDITION 2, CONDITION 3, dan seterusnya.

SIGNAL: Melempar Error Buatan Sendiri #

Kadang error yang kita butuhkan belum ada: harga negatif bukan pelanggaran sintaks, tetapi jelas tidak boleh masuk ke data. SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '...' melempar error kustom dari dalam prosedur — nilai 45000 adalah SQLSTATE yang disediakan khusus untuk error aplikasi:

sql
DROP PROCEDURE IF EXISTS atur_stok;
DELIMITER $$
CREATE PROCEDURE atur_stok(IN p_id INT, IN p_jumlah INT)
BEGIN
  IF p_jumlah < 0 THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'Jumlah stok tidak boleh negatif';
  END IF;

  UPDATE produk SET stok = p_jumlah WHERE id = p_id;
  SELECT ROW_COUNT() AS baris_diubah;
END$$
DELIMITER ;
CALL atur_stok(1, 50);

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+--------------+
| baris_diubah |
+--------------+
|            1 |
+--------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Pemanggilan valid berjalan normal: stok Kopi Gayo menjadi 50 dan ROW_COUNT() melaporkan satu baris berubah. Pemanggilan dengan nilai negatif memicu SIGNAL:

sql
-- @error
CALL atur_stok(1, -5);

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1644 (45000): Jumlah stok tidak boleh negatif
Bye

ERROR 1644 (45000): Jumlah stok tidak boleh negatif — pesan kita sendiri, bukan pesan bawaan server. Kode 1644 adalah nomor error MySQL untuk kondisi tak tertangani, sedangkan 45000 menandakan error buatan aplikasi. Pola IF invalid THEN SIGNAL inilah fondasi validasi terpusat di database: aturan yang sama menolak data buruk dari aplikasi mana pun.

RESIGNAL: Menangkap, Mencatat, lalu Meneruskan #

SIGNAL melempar error baru; RESIGNAL melempar ulang error yang sedang ditangani handler — setelah kita sempat mencatatnya. Prosedur atur_stok_log membungkus kegagalan apa pun dengan pencatatan ke tabel log_error sebelum meneruskannya ke pemanggil:

sql
DROP PROCEDURE IF EXISTS atur_stok_log;
DELIMITER $$
CREATE PROCEDURE atur_stok_log(IN p_id INT, IN p_jumlah INT)
BEGIN
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    INSERT INTO log_error (pesan) VALUES ('kegagalan di atur_stok_log');
    RESIGNAL;
  END;

  IF p_jumlah < 0 THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'Jumlah stok tidak boleh negatif';
  END IF;

  UPDATE produk SET stok = p_jumlah WHERE id = p_id;
  SELECT ROW_COUNT() AS baris_diubah;
END$$
DELIMITER ;

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Handler EXIT menangkap SQLEXCEPTION — termasuk SIGNAL milik kita sendiri. Isi handler mencatat kegagalan ke log_error, lalu RESIGNAL melempar error yang sama keluar dari prosedur:

sql
-- @error
CALL atur_stok_log(2, -5);

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1644 (45000): Jumlah stok tidak boleh negatif
Bye

Pemanggil tetap menerima ERROR 1644 — perhatikan bahwa tanpa RESIGNAL, handler EXIT akan menelan error dan CALL tampak sukses, menyembunyikan kegagalan dari aplikasi. Kini kita periksa jejak yang ditinggalkan:

sql
SELECT id, pesan FROM log_error ORDER BY id;

Output yang Dihasilkan:

text
+----+----------------------------+
| id | pesan                      |
+----+----------------------------+
|  1 | kegagalan di atur_stok_log |
+----+----------------------------+
1 row in set (0.00 sec)

Satu baris catatan kegagalan di atur_stok_log tersimpan — error sudah diteruskan ke pemanggil, tetapi audit trail-nya tinggal di database. Kombinasi handler + catat + RESIGNAL adalah pola baku untuk log terpusat.

Kode Error yang Sering Dijumpai #

Handler berbasis kode numerik menangkap kondisi secara presisi. Daftar berikut merangkum kode yang paling sering muncul di kehidupan sehari-hari:

KodeSQLSTATEPemicu
106223000nilai duplikat melanggar PRIMARY KEY atau UNIQUE
104823000kolom NOT NULL menerima NULL
145223000foreign key menunjuk baris induk yang tidak ada
114642S02tabel yang dirujuk tidak ada
105442S22kolom yang dirujuk tidak ada
1369HY000CHECK OPTION pada view menolak perubahan
164445000SIGNAL aplikasi yang tidak tertangani handler

Kode berakhiran kelas 23 (23000) semuanya pelanggaran integritas data — kelompok yang paling lazim ditangani handler karena datanya tetap konsisten meski statement ditolak. Kode 42 adalah kesalahan nama objek yang biasanya menandakan bug kode, bukan data, sehingga lebih baik dibiarkan meledak agar ketahuan saat pengembangan.

Membersihkan Database Latihan #

sql
DROP DATABASE belajar_sp;

Output yang Dihasilkan:

text
Query OK, 2 rows affected (0.00 sec)

Referensi #

Prosedur kita kini punya sistem saraf: handler menangkap error lewat kode, SQLSTATE, atau kelas; CONTINUE dan EXIT menentukan kelanjutan; GET DIAGNOSTICS membaca pesan aslinya; SIGNAL melempar aturan bisnis; RESIGNAL meneruskan setelah mencatat. Masih ada satu anggota keluarga stored program yang belum kita bahas — function, saudara prosedur yang mengembalikan nilai tunggal dan bisa dipanggil langsung di dalam SELECT. Chapter berikutnya membedahnya, termasuk aturan attribute DETERMINISTIC yang membuat function aman untuk binary log.