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:

KeluargaPenyebab umumSifatRespons yang tepat
Koneksi / server selectionserver mati, DNS salah, kredensial ditolaktransien (bisa pulih)coba-ulang dengan backoff; alarm bila menetap
TimeoutserverSelectionTimeoutMS, socketTimeoutMS terlampauitransiencoba-ulang operasi yang idempoten
Data ditolak serverduplicate key (E11000), validasi schema (121), write conflict transaksipermanen / berlabelperbaiki 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:

js
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:

text
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:

js
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:

text
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:

js Ilustrasi — coba-ulang dengan backoff untuk error transien
// 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:

js Ilustrasi — penanganan error yang membawa konteks
// 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:

js
db = db.getSiblingDB("latihan_error")
db.dropDatabase()

Output yang Dihasilkan:

text
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 di errInfo (nama property, operator, nilai yang dilanggar) dan errorLabels kosong.
  • Keputusan bercabang berdasarkan err.code yang stabil, bukan teks err.errmsg yang 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.