Dua puluh kontak seharusnya diarsipkan dalam sebuah CRM, namun UI justru menampilkan lencana sukses berwarna hijau dan operasi tersebut tidak melakukan apa-apa secara diam-diam. Sebuah helper TypeScript yang membuat kolom error menjadi wajib dalam mutasi Supabase-JS kini memaksa pengembang untuk menghadapi kegagalan tersebut alih-alih mengabaikannya begitu saja.
Mengapa bentuk pengembalian (return shape) Supabase-JS adalah sebuah jebakan
Klien JavaScript Supabase (@supabase/supabase-js) tidak melempar pengecualian (exception) saat penulisan basis data ditolak. Sebaliknya, ia menyelesaikan promise dengan sebuah objek { data, error }. Jika kebijakan row-level security (RLS), pelanggaran kunci unik (unique-key violation), atau batasan lainnya menghalangi kueri, data akan kembali sebagai null dan error akan berisi pesan basis data. Klien berasumsi bahwa pemanggil akan memeriksa error; ia tidak pernah membatalkan panggilan tersebut.
Dalam praktiknya, banyak basis kode memperlakukan panggilan tersebut sebagai operasi fire-and-forget:
await supabase
.from('contacts')
.update({ statut: 'ancien', archived_at: now })
.eq('id', id)
return { ok: true }
Ketika update diblokir oleh aturan RLS, promise tetap terselesaikan (resolves). Karena pengembang tidak pernah melakukan destrukturisasi { error }, kegagalan tersebut menjadi tidak terlihat. Fungsi mengembalikan { ok: true }, UI menunjukkan sukses, dan data tetap tidak berubah. Tidak ada log yang muncul, tidak ada peringatan Sentry yang menyala, dan bug tersebut bisa bertahan selama berhari-hari.
Linter saja tidak cukup
Alat analisis statis dapat memberikan peringatan saat properti error diabaikan, tetapi mereka tidak dapat memaksakan kontrak saat runtime. Seorang pengembang masih dapat menulis const _ = await … dan membungkam peringatan tersebut, atau mereka dapat menambahkan komentar untuk menekan aturan tersebut. Masalah mendasarnya adalah sistem tipe (type system) mengizinkan sebuah panggilan berhasil tanpa pernah menyebutkan error.
Helper mutate(): membuat penanganan kesalahan menjadi wajib
Penulis membuat sebuah wrapper kecil bernama mutate() yang mengubah bentuk tipe pengembaliannya. Alih-alih { data, error }, helper ini mengembalikan sebuah tuple [data, error] di mana error adalah kolom yang wajib diisi. TypeScript kemudian menolak untuk mengompilasi panggilan apa pun yang membuang elemen kedua.
async function mutate<T>(promise: Promise<{ data: T | null; error: any }>) {
const { data, error } = await promise
// Send error to Sentry immediately
if (error) Sentry.captureException(error)
return [data, error] as const
}
Penggunaannya menjadi eksplisit:
const [result, err] = await mutate(
supabase
.from('contacts')
.update({ statut: 'ancien', archived_at: now })
.eq('id', id)
)
if (err) {
// Handle or rethrow
return { ok: false, message: err.message }
}
return { ok: true, data: result }
Jika pengembang lupa menangkap err, TypeScript akan mengeluarkan kesalahan: “Tuple type [T, any] of length 2 has no element at index 1.” Kode tidak akan dapat dikompilasi sampai kesalahan tersebut ditangani. Helper ini juga menyuntikkan instrumentasi Sentry, menjamin bahwa setiap penolakan basis data dicatat meskipun pemanggil nantinya mengabaikannya.
Apa yang dipertaruhkan
- Integritas data – Kegagalan diam-diam membiarkan status yang tidak valid menyusup ke produksi. Kontak yang diarsipkan namun tidak pernah keluar dari daftar aktif dapat menyebabkan laporan hilir menjadi salah.
- Kepercayaan pengguna – UI yang mengklaim sukses padahal tidak ada yang berubah akan mengikis kepercayaan. Pelanggan melihat status "diarsipkan" tetapi masih menemukan kontak tersebut dalam pencarian.
- Waktu pengembang – Mengejar bug hantu memakan waktu berjam-jam. Penanganan kesalahan yang eksplisit memunculkan masalah pada titik kegagalan, sehingga memperpendek siklus debugging.
- Biaya operasional – Menambahkan beberapa baris kode ekstra dan sebuah wrapper kecil tidaklah berarti dibandingkan dengan biaya insiden kehilangan data secara diam-diam.
Kompromi (trade-off)
Helper ini menambah verbositas: setiap panggilan mutasi sekarang mengembalikan sebuah tuple, dan pemanggil harus menulis blok if (err). Beberapa tim mungkin menganggap ini sebagai kebisingan boilerplate, terutama untuk tindakan CRUD sederhana di mana mereka mengharapkan keberhasilan. Argumen tandingannya adalah bahwa kode tambahan tersebut merupakan pengaman, bukan fitur opsional. Dalam lingkungan di mana kebenaran data sangat penting—sistem CRM, keuangan, kesehatan—memaksa pengecekan ini akan sangat sepadan.
Apa selanjutnya
Penulis menjanjikan bagian ketiga yang akan memeriksa kebijakan RLS yang mengembalikan nol baris tanpa penjelasan. Pola tersebut, seperti halnya kasus kesalahan diam-diam, menyembunyikan kegagalan di balik kueri yang tampak berhasil. Secara keseluruhan, seri ini bertujuan untuk mengungkap sisi "rumor" dari API Supabase dan memberikan alat konkret bagi pengembang untuk menuntut fakta.
Kesimpulan: Desain Supabase-JS memungkinkan penulisan yang gagal tampak seperti sukses kecuali jika Anda ingat untuk membaca kolom error. Dengan membungkus panggilan dalam helper yang dipaksakan oleh TypeScript yang membuat error menjadi wajib dan mencatatnya ke Sentry, Anda mengubah kegagalan diam-diam menjadi peristiwa yang terlihat dan dapat ditindaklanjuti. Peningkatan kecil dalam ukuran kode memberikan Anda keandalan data dan kepercayaan pengguna—dua hal yang tidak akan pernah bisa diberikan oleh keberhasilan yang diam-diam.
