Saat Siti menekan tombol bayar, server toko buku kebetulan sedang kelebihan beban dan menjawab 503 Service Unavailable. Menampilkan pesan galat seketika merugikan dua pihak: pengguna kehilangan transaksi, padahal server hanya butuh beberapa detik untuk pulih. Permintaan semacam ini pantas dicoba ulang (retry) — tetapi dengan strategi, bukan dengan pengulangan buta yang membanjiri server yang sudah sibuk. Chapter ini merancang strateginya: kapan boleh mencoba ulang, jeda berapa lama di antara percobaan, dan kapan permintaan perlu diberi batas waktu.
Kedua berkas contoh memakai server tiruan yang sengaja digarap seperti itu: satu endpoint gagal dua kali lalu pulih, satu lagi lambat melewati batas waktu pada percobaan pertama. Jalankan node coba-ulang-backoff.mjs dan node batas-waktu-percobaan.mjs untuk mereproduksi keluarannya.
Tiga Aturan Main Retry #
Pengulangan yang salah memperberat server yang sedang susah. Tiga aturan menjaganya tetap sehat:
- Hanya kegagalan sementara yang diulang. Status
5xx(terutama503) dan kegagalan jaringan sepertiTimeoutErrorlayak dicoba lagi karena kondisinya bisa pulih. Sebaliknya,4xxmenandakan permintaan itu sendiri salah — permintaan identik berikutnya akan gagal dengan cara yang sama, jadi jangan diulang. - Beri jeda antar percobaan (backoff). Percobaan yang menyusul tanpa jeda hanya menambah tekanan. Jeda yang lazim bertumbuh eksponensial: percobaan kedua menunggu dua kali lebih lama dari yang pertama, percobaan ketiga dua kali lagi.
- Tetapkan batas percobaan (max attempt). Tanpa batas, loop retry bisa berjalan tanpa henti. Tiga sampai lima percobaan adalah titik berhenti yang wajar; setelah itu lemparkan galat agar penanganan di lapisan atas bekerja.
Jeda eksponensial ini disebut exponential backoff. Rumus umumnya jedaAwal * 2 ** (percobaan - 1) — dari jeda awal 10 ms menghasilkan 10, 20, 40 ms, dan seterusnya. Contoh kita memakai angka mini agar keluaran cepat teruji; sistem nyata biasanya memulai dari ratusan milidetik. Implementasi produksi sering menambahkan jitter — pengacakan kecil pada jeda — supaya ribuan client tidak menyerang server di detik yang sama; konsepnya di luar cakupan contoh ini.
Satu penyetelan yang murah namun berdampak: hormati jawaban server. Status 503 atau 429 sering datang dengan header Retry-After yang mengajukan jeda tunggu — bila server berkata “tunggu 30 detik”, mengabaikannya dan mencoba 100 ms kemudian justru menandakan client yang tak bisa diajak sepakat. Bila header itu ada, pakai; bila tidak, rumus backoff di atas yang memutuskan. Dalam loop pagination dari chapter sebelumnya, pola yang lazim adalah membungkus fetch tiap halaman dengan helper retry — satu halaman gagal sementara tak perlu membatalkan penelusuran seluruh katalog.
Retry dengan Backoff terhadap Server yang Pulih #
Berkas berikut menuntut dua kasus sekaligus: server yang gagal dua kali lalu pulih, dan server yang tidak pernah pulih — untuk membuktikan batas percobaan benar-benar berhenti:
// coba-ulang-backoff.mjs
// Pola retry dengan backoff eksponensial terhadap server tiruan yang gagal 2x.
// Jalankan di terminal: node coba-ulang-backoff.mjs (tanpa internet).
import http from "node:http";
// Server tiruan: /bayar gagal 2x (503) lalu pulih; /selalu-gagal selalu 503.
let gagalBerapa = 0;
const server = http.createServer((req, res) => {
if (req.url === "/bayar") {
if (gagalBerapa < 2) {
gagalBerapa++;
res.writeHead(503, { "Content-Type": "application/json" });
res.end(JSON.stringify({ pesan: "sedang sibuk, coba lagi" }));
} else {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "LUNAS", id: "INV-092" }));
}
return;
}
res.writeHead(503, { "Content-Type": "application/json" });
res.end(JSON.stringify({ pesan: "server tidak pulih" }));
});
const tunggu = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// fetchDenganRetry: mencoba hingga maxPercobaan kali, jeda menggandakan diri.
async function fetchDenganRetry(url, { maxPercobaan = 3, jedaAwal = 10 } = {}) {
for (let percobaan = 1; percobaan <= maxPercobaan; percobaan++) {
const response = await fetch(url);
if (response.ok) return response;
const gagalTerakhir = percobaan === maxPercobaan;
console.log(`percobaan ${percobaan}: status ${response.status} — ${gagalTerakhir ? "berhenti" : "akan diulang"}`);
if (gagalTerakhir) break;
const jeda = jedaAwal * 2 ** (percobaan - 1); // 10, 20, 40, ... ms
console.log(`menunggu ${jeda} ms...`);
await tunggu(jeda);
}
throw new Error(`gagal setelah ${maxPercobaan} percobaan`);
}
server.listen(0, "127.0.0.1", async () => {
const port = server.address().port;
const dasar = `http://127.0.0.1:${port}`;
// Kasus 1: server pulih pada percobaan ketiga.
const pulih = await fetchDenganRetry(`${dasar}/bayar`);
console.log("sukses pulih:", pulih.status, "-", JSON.stringify(await pulih.json()));
// Kasus 2: server tidak pernah pulih — galat dilempar setelah batas percobaan.
try {
await fetchDenganRetry(`${dasar}/selalu-gagal`);
} catch (error) {
console.log("tertangkap:", error.message);
}
server.closeAllConnections();
server.close();
});Output yang Dihasilkan:
percobaan 1: status 503 — akan diulang
menunggu 10 ms...
percobaan 2: status 503 — akan diulang
menunggu 20 ms...
sukses pulih: 200 - {"status":"LUNAS","id":"INV-092"}
percobaan 1: status 503 — akan diulang
menunggu 10 ms...
percobaan 2: status 503 — akan diulang
menunggu 20 ms...
percobaan 3: status 503 — berhenti
tertangkap: gagal setelah 3 percobaanMari kita telaah baris-baris pentingnya:
- Baris 8 dan 11–14 (server yang gagal terjadwal): variabel
gagalBerapamembuat endpoint/bayarmenjawab503untuk dua permintaan pertama, lalu200setelahnya — perilaku server yang pulih, yang tidak mungkin diuji dengan internet sungguhan secara deterministik. - Baris 29–31 (loop percobaan):
forberjalan maksimalmaxPercobaankali. Begituresponse.okbernilaitrue, function langsung mengembalikan respons sukses — percobaan sisa tidak pernah berjalan. - Baris 33–35 (deteksi putus):
gagalTerakhirmemisahkan dua nasib: percobaan yang masih punya kesempatan dicatat “akan diulang”, sedangkan percobaan terakhir dicatat “berhenti” lalu keluar dari loop. - Baris 36 (
jedaAwal * 2 ** (percobaan - 1)): inti backoff eksponensial — pangkat yang naik bersama nomor percobaan menghasilkan 10, 20, 40 ms. Operator**dari section Operators bekerja di sini. - Baris 38 (
await tunggu(jeda)): jeda menunggu sungguhan;tungguadalah pembungkussetTimeoutdalamPromisesehingga bisa di-awaitdi dalam function async. - Baris 40 (galat akhir): setelah batas percobaan habis, function melempar
Errordengan pesan ringkas. Kegagalan jadi eksplisit dan tertangani — keluaran kasus kedua membuktikannya:tertangkap: gagal setelah 3 percobaan.
Perhatikan juga keluaran kasus pertama: tidak ada baris “percobaan 3” untuk /bayar, karena percobaan ketiga langsung sukses dan return di baris 31 mengakhiri semuanya.
Inilah alasan mock server seperti ini bernilai: perilaku “gagal dua kali lalu pulih” nyaris mustahil direproduksi secara deterministik terhadap API sungguhan, tetapi dengan variabel penghitung sederhana, pola retry kita teruji berulang kali dengan keluaran yang sama — persis filosofi pengujian yang dipakai sepanjang section ini.
Batas Waktu per Percobaan #
Retry tanpa batas waktu punya celah: percobaan yang menggantung tanpa jawaban akan memegang loop berjam-jam. Gabungan yang sehat menyatukan dua chapter sebelumnya — tiap percobaan diberi sinyal AbortSignal.timeout, dan TimeoutError diperlakukan sebagai kegagalan sementara yang layak dicoba lagi:
// batas-waktu-percobaan.mjs
// Retry + timeout: tiap percobaan diberi batas waktu lewat AbortSignal.timeout.
// Jalankan di terminal: node batas-waktu-percobaan.mjs (tanpa internet).
import http from "node:http";
// Server tiruan: permintaan pertama ke /ambil berjalan lambat, selanjutnya cepat.
let sudahDitanya = 0;
const server = http.createServer((req, res) => {
if (req.url === "/ambil") {
if (sudahDitanya === 0) {
sudahDitanya++;
setTimeout(() => res.end('{"data":"katalog lengkap"}'), 400); // melewati batas 50 ms
return;
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end('{"data":"katalog lengkap"}');
return;
}
res.writeHead(404);
res.end();
});
const tunggu = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function fetchDenganRetry(url, { maxPercobaan = 3, batasWaktu = 50, jedaAwal = 10 } = {}) {
for (let percobaan = 1; percobaan <= maxPercobaan; percobaan++) {
try {
const response = await fetch(url, { signal: AbortSignal.timeout(batasWaktu) });
if (response.ok) return response;
console.log(`percobaan ${percobaan}: status ${response.status}`);
} catch (error) {
// TimeoutError berarti percobaan ini melebihi batas waktu — layak diulang.
console.log(`percobaan ${percobaan}: ${error.name} — batas ${batasWaktu} ms terlampaui`);
}
if (percobaan < maxPercobaan) {
const jeda = jedaAwal * 2 ** (percobaan - 1);
console.log(`menunggu ${jeda} ms...`);
await tunggu(jeda);
}
}
throw new Error(`gagal setelah ${maxPercobaan} percobaan`);
}
server.listen(0, "127.0.0.1", async () => {
const port = server.address().port;
const response = await fetchDenganRetry(`http://127.0.0.1:${port}/ambil`);
console.log("sukses:", response.status, "-", JSON.stringify(await response.json()));
server.closeAllConnections();
server.close();
});Output yang Dihasilkan:
percobaan 1: TimeoutError — batas 50 ms terlampaui
menunggu 10 ms...
sukses: 200 - {"data":"katalog lengkap"}Mari kita telaah baris-baris pentingnya:
- Baris 11–14 (lambat hanya sekali): server tiruan menjawab permintaan pertama setelah 400 ms — di luar batas 50 ms — dan responsnya nyaris seketika untuk permintaan berikutnya, sehingga cerita retry-nya deterministik.
- Baris 29 (
{ signal: AbortSignal.timeout(batasWaktu) }): tiap percobaan memperoleh sinyal waktunya sendiri — batas berlaku per percobaan, bukan untuk seluruh rangkaian. - Baris 32–34 (galat waktu = sinyal diulang):
TimeoutErrortertangkap, dicatat, dan loop melanjutkan ke percobaan berikutnya alih-alih gagal total. Inilah bedanya denganAbortErrorbuatan pengguna, yang biasanya berarti “berhenti, jangan diulang”. - Baris 36 (
if (percobaan < maxPercobaan)): jeda hanya dijalankan bila masih ada percobaan sisa — tidak ada penantian sia-sia setelah putaran terakhir.
Strategi retry menuntaskan ketahanan sisi client. Pertanyaan yang tersisa: ketika semua percobaan gagal, bagaimana menerjemahkan respons galat server menjadi sesuatu yang bermakna bagi pengguna? Itulah topik penutup section ini — API Error Handling.