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):
// 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)
})MONGODB_URI="mongodb://127.0.0.1:27017/?directConnection=true" node kenali-galat.jsOutput yang Dihasilkan:
== 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:27099Galat 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 galat | Asal | Pemicu umum | err.code |
|---|---|---|---|
MongoServerError | jawaban galat server | duplicate key, validasi, otorisasi | terisi (11000, 13, …) |
MongoNetworkError | koneksi putus saat operasi berjalan | gangguan jaringan, restart server | umumnya kosong |
MongoServerSelectionError | driver tidak menemukan server sehat | server mati, URI salah | kosong |
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:
// 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)
})MONGODB_URI="mongodb://127.0.0.1:27017/?directConnection=true" node retry-transien.jsOutput yang Dihasilkan:
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: MongoNetworkErrorKegagalan 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:
| Opsi | Sisi | Mengikat |
|---|---|---|
serverSelectionTimeoutMS | client | menunggu server sehat tersedia |
connectTimeoutMS | client | membuka koneksi TCP/TLS per koneksi |
socketTimeoutMS | client | menunggu jawaban per operasi lewat socket |
maxTimeMS (per operasi) | server | batas 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:
// 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.nameuntuk kelas galat,err.codeuntuk kode server (11000 = duplicate key), danerr.keyValueuntuk field yang bentrok. MongoServerErrordatang dari server dan membawacode;MongoNetworkError/MongoServerSelectionErrorlahir di sisi client tanpacode.- 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,
withTransactionmenangani coba-ulang transien. serverSelectionTimeoutMS,connectTimeoutMS,socketTimeoutMSmemagari sisi client;maxTimeMSmemagari eksekusi di server.