Aplikasi hidup di dunia yang penuh kegagalan: jaringan terputus sebentar, dua pengguna memesan kursi terakhir bersamaan, input tidak lengkap dikirim melewati form. Driver MongoDB melaporkan semua kejadian itu sebagai error — dan kualitas aplikasi ditentukan oleh cara kode menanggapinya. Error yang di-retry padahal bersifat permanen membuang waktu; error yang ditelan diam-diam menyembunyikan kehilangan data. Chapter ini mengklasifikasikan error yang paling sering dijumpai, menampilkan bentuk aslinya lewat eksekusi sungguhan, lalu menyusun pola respons yang tepat untuk masing-masing.
Tiga Keluarga Error #
Sebelum menangani, kenali dulu sifatnya. Dari sisi aplikasi, error terbagi tiga keluarga:
| Keluarga | Penyebab umum | Sifat | Respons yang tepat |
|---|---|---|---|
| Koneksi / server selection | server mati, DNS salah, kredensial ditolak | transien (bisa pulih) | coba-ulang dengan backoff; alarm bila menetap |
| Timeout | serverSelectionTimeoutMS, socketTimeoutMS terlampaui | transien | coba-ulang operasi yang idempoten |
| Data ditolak server | duplicate key (E11000), validasi schema (121), write conflict transaksi | permanen / berlabel | perbaiki input, atau ulangi transaksi bila berlabel |
Kunci keputulannya satu pertanyaan: bila operasi ini diulang dengan input yang sama, apa hasilnya? Bila hasilnya pasti sama-sama gagal, error itu permanen dan harus diperbaiki, bukan diulang. Bila penyebabnya kondisi lingkungan (jaringan, pemilihan primary baru), error itu transien dan layak dicoba ulang.
Error Permanen yang Kita Buat Sendiri #
Dua error paling sering muncul di aplikasi justru berasal dari aturan yang kita pasang sendiri di database: unique index dan schema validator. Kita timbulkan keduanya dan amati bentuk aslinya. Pertama, duplicate key:
db = db.getSiblingDB("latihan_error")
db.pengguna.drop()
db.pengguna.createIndex({ email: 1 }, { unique: true })
db.pengguna.insertOne({ nama: "Budi", email: "[email protected]" })
try {
db.pengguna.insertOne({ nama: "Budi Kembar", email: "[email protected]" })
} catch (e) {
print("code:", e.code)
print("pesan:", e.errmsg)
}Output yang Dihasilkan:
latihan_error
true
email_1
{
acknowledged: true,
insertedId: ObjectId('6ad3e073777e19e421eeca69')
}
code: 11000
pesan: E11000 duplicate key error collection: latihan_error.pengguna index: email_1 dup key: { email: "[email protected]" }Baris code: 11000 adalah kode numerik yang stabil antar versi — inilah yang dicek aplikasi, bukan teks pesannya. Pesan lengkapnya menyebut collection, nama index (email_1), dan nilai yang menabrak; teks ini boleh berubah format antar versi server, jadi jangan diandalkan untuk keputusan bercabang.
Kedua, penolakan schema validator. Kita buat collection dengan aturan sederhana — harga wajib berupa angka positif:
db = db.getSiblingDB("latihan_error")
db.dropDatabase()
db.createCollection("produk", {
validator: { $jsonSchema: {
bsonType: "object",
required: ["nama", "harga"],
properties: {
nama: { bsonType: "string" },
harga: { bsonType: "int", minimum: 0 }
}
} }
})
try {
db.produk.insertOne({ nama: "Kopi Gayo", harga: -5000 })
} catch (e) {
print("name:", e.name, "| code:", e.code)
print("errorLabels:", JSON.stringify(e.errorLabels))
print("errInfo:", JSON.stringify(e.errInfo))
}Output yang Dihasilkan:
latihan_error
{ ok: 1, dropped: 'latihan_error' }
{ ok: 1 }
name: MongoServerError | code: 121
errorLabels: []
errInfo: {"failingDocumentId":"6ad3e073777e19e421eeca6b","details":{"operatorName":"$jsonSchema","schemaRulesNotSatisfied":[{"operatorName":"properties","propertiesNotSatisfied":[{"propertyName":"harga","details":[{"operatorName":"minimum","specifiedAs":{"minimum":0},"reason":"comparison failed","consideredValue":-5000}]}]}]}}Tiga hal terbaca dari output itu: name: MongoServerError dengan code: 121 menandai penolakan validator (bukan masalah server), errorLabels yang kosong menegaskan error ini tidak punya label coba-ulang, dan errInfo merinci aturan mana yang dilanggar — propertyName: "harga", operatorName: "minimum", consideredValue: -5000. Inilah bahan mentah untuk diterjemahkan menjadi pesan yang ramah bagi pengguna; strategi lengkapnya kita bahas di Chapter Schema Validation di Aplikasi.
Error Transien dan Pola Coba-Ulang #
Error koneksi dan timeout bersifat transien: kondisi lingkungannya bisa membaik. Pola standarnya adalah coba-ulang dengan exponential backoff — jeda antar percobaan menggandakan (1 detik, 2, 4, …) dengan jumlah maksimum dan sedikit jitter agar banyak instance tidak mencoba pada detik yang sama:
// mdtest: skip — pola umum di aplikasi Node.js
async function denganCobaUlang(fn, maksPercobaan = 3) {
for (let percobaan = 1; ; percobaan++) {
try {
return await fn()
} catch (err) {
const transien = err.name === "MongoNetworkError" ||
err.name === "MongoServerSelectionError" ||
err.name === "MongoTimeoutError"
if (!transien || percobaan >= maksPercobaan) throw err
const jeda = 2 ** percobaan * 100 + Math.random() * 100
await new Promise(r => setTimeout(r, jeda))
}
}
}Tiga syarat wajib pola ini: hanya error transien yang diulang (dicek lewat nama/kode), jumlah percobaan dibatasi, dan operasinya idempoten. Untuk transaksi, coba-ulangnya tidak perlu kita rakit sendiri — server memberi label TransientTransactionError dan UnknownTransactionCommitResult pada error yang aman diulang, dan withTransaction() memproses label itu otomatis (kita praktikkan di Chapter Transaksi dari Aplikasi).
Log Konteks, Bukan Sekadar Pesan #
Error yang sampai ke log harus membawa konteks untuk didiagnosis: operasi apa, filter atau document apa, dan ID korelasi request. Dua batasnya: jangan pernah mencatat kredensial atau data pribadi pengguna secara utuh (amarkan connection string seperti di Chapter Credential Management), dan jangan mengandalkan err.message satu-satunya — sertakan err.code untuk keputusan bercabang.
Pola ringkasnya:
// mdtest: skip — kerangka penanganan di aplikasi
try {
await koleksi.insertOne(pesananBaru)
} catch (err) {
if (err.code === 11000) {
// permanen: input bentrok — laporkan ke pengguna, jangan diulang
log.warn({ kode: err.code, koleksi: "pesanan" }, "pesanan duplikat ditolak")
throw new GalatInput("Pesanan dengan kode ini sudah ada")
}
if (err.code === 121) {
// permanen: terjemahkan aturan yang dilanggar menjadi pesan pengguna
log.warn({ kode: err.code }, "validasi schema menolak document")
throw new GalatInput(terjemahkanValidasi(err.errInfo))
}
// sisanya: lempar apa adanya — middleware error di atas yang memutuskan
throw err
}Terakhir, kita rapikan sisa latihan:
db = db.getSiblingDB("latihan_error")
db.dropDatabase()Output yang Dihasilkan:
latihan_error
{ ok: 1, dropped: 'latihan_error' }Rangkuman #
- Tiga keluarga error: koneksi/server selection dan timeout (transien — layak dicoba ulang), serta data ditolak server (permanen — perbaiki inputnya).
code: 11000= duplicate key pada unique index;code: 121= penolakan validator dengan detail aturan dierrInfo(nama property, operator, nilai yang dilanggar) danerrorLabelskosong.- Keputusan bercabang berdasarkan
err.codeyang stabil, bukan tekserr.errmsgyang bisa berubah format. - Coba-ulang hanya untuk error transien, dengan backoff, batas percobaan, dan operasi idempoten; transaksi punya label coba-ulangnya sendiri.
- Jangan telan error: log konteks (operasi, kode, ID korelasi) dan terjemahkan menjadi pesan yang jelas bagi pengguna.