Aplikasi yang berjalan lama pasti berjumpa galat: jaringan terputus sesaat, server sedang memilih node baru, atau penulisan ditolak karena data tidak unik. Yang membedakan aplikasi tangguh dari yang rapuh bukan bebas galat, melainkan cara menanganinya: galat mana yang cukup dilaporkan, galat mana yang boleh dicoba ulang, dan berapa lama boleh menunggu sebelum menyerah. Driver Node.js membantu klasifikasi itu lewat nama kelas error dan field seperti code. Chapter ini membaca anatomi galat dari eksekusi nyata, lalu membangun pola coba-ulang dengan batas percobaan dan jeda berlipat.

Membaca Galat: name, code, dan keyValue #

Setiap galat driver membawa tiga petunjuk utama: err.name (kelas galat), err.code (kode numerik dari server, bila ada), dan err.message. File berikut memicu dua keluarga galat sekaligus — penolakan dari server (duplicate key) dan kegagalan dari sisi client (server tidak pernah dijangkau):

js
// kenali-galat.js — mengenali kelas galat driver lewat field-nya
// Jalankan: MONGODB_URI="mongodb://127.0.0.1:27017/?directConnection=true" node kenali-galat.js
const { MongoClient } = require("mongodb")

const uri = process.env.MONGODB_URI ?? "mongodb://127.0.0.1:27017/?directConnection=true"

async function main() {
  const client = new MongoClient(uri)
  await client.connect()
  const buku = client.db("latihan_js").collection("buku")
  await buku.drop().catch(() => {})
  await buku.createIndex({ isbn: 1 }, { unique: true })
  await buku.insertOne({ judul: "Bumi Manusia", penulis: "Pramoedya A. Toer", isbn: "978-979-9731-23-4" })

  // ===== Galat dari server: duplicate key pada index unik =====
  try {
    await buku.insertOne({ judul: "Bumi Manusia (salinan)", isbn: "978-979-9731-23-4" })
  } catch (err) {
    console.log("== galat dari server ==")
    console.log("name    :", err.name)
    console.log("code    :", err.code)
    console.log("pesan   :", err.message.split("\n")[0])
    console.log("keyValue:", JSON.stringify(err.keyValue))
  }
  await client.close()

  // ===== Galat dari sisi client: server tidak pernah dijangkau =====
  const clientMati = new MongoClient("mongodb://127.0.0.1:27099/?directConnection=true&serverSelectionTimeoutMS=1500")
  try {
    await clientMati.connect()
  } catch (err) {
    console.log("== galat dari client ==")
    console.log("name    :", err.name)
    console.log("code    :", err.code)
    console.log("pesan   :", err.message.split("\n")[0])
  } finally {
    await clientMati.close()
  }
}

main().catch(err => {
  console.error("GAGAL:", err.name, "-", err.message)
  process.exit(1)
})
bash
MONGODB_URI="mongodb://127.0.0.1:27017/?directConnection=true" node kenali-galat.js

Output yang Dihasilkan:

text
== galat dari server ==
name    : MongoServerError
code    : 11000
pesan   : E11000 duplicate key error collection: latihan_js.buku index: isbn_1 dup key: { isbn: "978-979-9731-23-4" }
keyValue: {"isbn":"978-979-9731-23-4"}
== galat dari client ==
name    : MongoServerSelectionError
code    : undefined
pesan   : connect ECONNREFUSED 127.0.0.1:27099

Galat pertama lahir di server: MongoServerError dengan code: 11000 — kode tetap untuk duplicate key, sehingga penanganannya bisa spesifik (mis. membalas “ISBN sudah terdaftar” ke pengguna). Field keyValue bahkan menyebut pasangan field-nilai yang bentrok, tanpa perlu mengurai pesan. Galat kedua lahir sebelum server sempat menjawab: MongoServerSelectionError tanpa code, karena keputusannya diambil driver sendiri setelah batas serverSelectionTimeoutMS terlampaui. Peta lengkapnya:

Kelas galatAsalPemicu umumerr.code
MongoServerErrorjawaban galat serverduplicate key, validasi, otorisasiterisi (11000, 13, …)
MongoNetworkErrorkoneksi putus saat operasi berjalangangguan jaringan, restart serverumumnya kosong
MongoServerSelectionErrordriver tidak menemukan server sehatserver mati, URI salahkosong

Pola Coba-Ulang untuk Galat Transien #

Prinsipnya satu kalimat: galat transien (kegagalan sesaat) boleh dicoba ulang, galat permanen tidak. Duplicate key tidak akan berubah dicoba tiga kali — data tetap bentrok — sedangkan gangguan jaringan sering pulih dalam hitungan ratusan milidetik. Pola amannya: klasifikasi dulu, beri batas percobaan, dan beri jeda yang membesar (backoff) agar aplikasi tidak menghujani server yang sedang sibuk:

js
// retry-transien.js — pola coba-ulang untuk galat transien (maks 3 percobaan)
// Jalankan: MONGODB_URI="mongodb://127.0.0.1:27017/?directConnection=true" node retry-transien.js
const { MongoClient } = require("mongodb")

const uri = process.env.MONGODB_URI ?? "mongodb://127.0.0.1:27017/?directConnection=true"

// Kelas galat yang layak dicoba ulang: kegagalan sesaat, bukan kesalahan data
const GALAT_TRANSIEN = new Set(["MongoNetworkError", "MongoServerSelectionError"])

async function denganRetry(fn, { maksPercobaan = 3, jedaAwal = 200 } = {}) {
  let percobaan = 0
  while (true) {
    percobaan++
    try {
      const hasil = await fn()
      console.log("  percobaan", percobaan, "berhasil")
      return hasil
    } catch (err) {
      if (percobaan >= maksPercobaan || !GALAT_TRANSIEN.has(err.name)) throw err
      const jeda = jedaAwal * 2 ** (percobaan - 1) // jeda berlipat: 200 ms, 400 ms
      console.log(`  percobaan ${percobaan} gagal (${err.name}) — tunggu ${jeda} ms`)
      await new Promise(r => setTimeout(r, jeda))
    }
  }
}

function galatJaringan(pesan) {
  const err = new Error(pesan)
  err.name = "MongoNetworkError"
  return err
}

async function main() {
  const client = new MongoClient(uri)
  await client.connect()
  const pesanan = client.db("latihan_js").collection("pesanan")
  await pesanan.drop().catch(() => {})

  // ===== Kasus 1: dua kegagalan sesaat, tulisan sungguhan di percobaan ketiga =====
  let sisaKegagalan = 2
  const hasil = await denganRetry(async () => {
    if (sisaKegagalan-- > 0) throw galatJaringan("simulasi: koneksi terputus saat menulis")
    return pesanan.insertOne({ pelanggan: "Dewi", total: 155000 })
  })
  console.log("hasil   : insertedId =", hasil.insertedId)
  console.log("tersimpan:", await pesanan.countDocuments(), "pesanan")

  // ===== Kasus 2: selalu gagal — berhenti tepat di batas percobaan =====
  try {
    await denganRetry(async () => { throw galatJaringan("simulasi: gangguan berlanjut") })
  } catch (err) {
    console.log("berhenti setelah batas percobaan — terakhir:", err.name)
  }

  await pesanan.drop()
  await client.close()
}

main().catch(err => {
  console.error("GAGAL:", err.name, "-", err.message)
  process.exit(1)
})
bash
MONGODB_URI="mongodb://127.0.0.1:27017/?directConnection=true" node retry-transien.js

Output yang Dihasilkan:

text
  percobaan 1 gagal (MongoNetworkError) — tunggu 200 ms
  percobaan 2 gagal (MongoNetworkError) — tunggu 400 ms
  percobaan 3 berhasil
hasil   : insertedId = new ObjectId('6ad3f16c1d675b339c81d529')
tersimpan: 1 pesanan
  percobaan 1 gagal (MongoNetworkError) — tunggu 200 ms
  percobaan 2 gagal (MongoNetworkError) — tunggu 400 ms
berhenti setelah batas percobaan — terakhir: MongoNetworkError

Kegagalan jaringan di sini sengaja disimulasikan agar polanya terlihat deterministik — di produksi, sumbernya gangguan sungguhan, tetapi percabangan kodenya sama. Mari kita bedah denganRetry: pemeriksaan !GALAT_TRANSIEN.has(err.name) dilempar langsung tanpa percobaan ulang — itulah jalur keluar bagi galat permanen seperti MongoServerError 11000. Baris jedaAwal * 2 ** (percobaan - 1) menghasilkan jeda 200 lalu 400 milidetik; pertumbuhan eksponensial ini memberi waktu pulih sekaligus membatasi beban. Kasus kedua membuktikan batasnya: setelah percobaan ketiga, galat terakhir diteruskan ke pemanggil — penungguan tanpa akhir tidak terjadi.

Timeout yang Memotong Penungguan #

Penanganan galat yang baik dimulai dari batas waktu yang jelas — tanpanya, “menunggu” adalah satu-satunya perilaku. Opsi-opsi berikut membentuk pagar tersebut:

OpsiSisiMengikat
serverSelectionTimeoutMSclientmenunggu server sehat tersedia
connectTimeoutMSclientmembuka koneksi TCP/TLS per koneksi
socketTimeoutMSclientmenunggu jawaban per operasi lewat socket
maxTimeMS (per operasi)serverbatas eksekusi operasi di server

Tiga yang pertama diatur di konstruktor MongoClient (atau di URI, seperti serverSelectionTimeoutMS=1500 pada contoh kenali-galat.js), sedangkan maxTimeMS melekat pada operasi — berguna untuk melindungi query berat agar tidak menyandera aplikasi:

js maxTimeMS membatasi eksekusi di sisi server
// mdtest: skip — ilustrasi opsi per-operasi
const laporan = await penjualan.aggregate(pipelineBerat, { maxTimeMS: 5000 }).toArray()

Bila server melewati maxTimeMS, operasi dibatalkan dan aplikasi menerima galat — satu-satunya cara memastikan query analitik selama tidak pernah lebih lama dari lima detik, apa pun kondisi datanya.

Rangkuman #

  • Baca err.name untuk kelas galat, err.code untuk kode server (11000 = duplicate key), dan err.keyValue untuk field yang bentrok.
  • MongoServerError datang dari server dan membawa code; MongoNetworkError/MongoServerSelectionError lahir di sisi client tanpa code.
  • Coba ulang hanya galat transien, dengan batas percobaan (mis. 3) dan jeda berlipat; galat permanen langsung diteruskan.
  • Pastikan penulisan yang diulang bersifat idempoten (unique index + idempotency key); di dalam transaksi, withTransaction menangani coba-ulang transien.
  • serverSelectionTimeoutMS, connectTimeoutMS, socketTimeoutMS memagari sisi client; maxTimeMS memagari eksekusi di server.