Mengelola Retry dan Exponential Backoff untuk HTTP Request di Frontend: Membangun Aplikasi Web yang Tahan Banting
1. Pendahuluan
Pernahkah Anda mengalami aplikasi web yang tiba-tiba “macet” atau menampilkan pesan error generik saat koneksi internet sedikit goyah, atau ketika server backend sedang sibuk? Pengalaman seperti ini tentu sangat menjengkelkan bagi pengguna. Di dunia web development, kita sering berasumsi bahwa jaringan selalu stabil dan server selalu responsif. Namun, realitanya jauh dari itu. Koneksi internet yang putus-nyambung, server yang kelebihan beban, atau bahkan pembaruan backend yang menyebabkan downtime singkat adalah hal yang lumrah.
Di sinilah strategi retry dan exponential backoff menjadi sangat krusial, terutama untuk HTTP request yang kita lakukan dari sisi frontend. Bayangkan Anda sedang mengisi formulir penting, lalu tiba-tiba request simpan gagal karena masalah jaringan sesaat. Tanpa mekanisme retry yang cerdas, pengguna harus mengulang semua dari awal. Dengan retry, aplikasi bisa mencoba lagi secara otomatis, meningkatkan peluang request berhasil tanpa campur tangan pengguna, dan pada akhirnya, memberikan pengalaman pengguna yang jauh lebih mulus dan andal.
Artikel ini akan membawa Anda menyelami bagaimana mengimplementasikan strategi retry dan exponential backoff yang efektif di aplikasi frontend Anda. Kita akan membahas konsep dasar, kapan harus menggunakannya, cara implementasi manual dengan JavaScript/TypeScript, hingga memanfaatkan library populer dan best practices yang perlu Anda pertimbangkan. Mari kita bangun aplikasi web yang lebih tahan banting! 🎯
2. Memahami Retry dan Exponential Backoff
Sebelum melangkah ke implementasi, mari kita pahami dulu apa itu retry dan kenapa exponential backoff sangat penting.
Retry Dasar: Coba Lagi Setelah Gagal
Konsep retry sangat sederhana: jika sebuah request gagal, coba lagi. Ini adalah pendekatan paling dasar untuk menangani kegagalan sementara. Misalnya, jika request API gagal dengan status 500 Internal Server Error, ada kemungkinan itu hanya masalah sesaat di server. Dengan retry, kita bisa mencoba mengirim request yang sama beberapa saat kemudian.
Namun, retry dasar memiliki masalah besar: ❌ Thundering Herd Problem: Jika banyak klien mencoba me-retry request secara bersamaan setelah server down, mereka semua akan mencoba di waktu yang sama, membanjiri server yang baru pulih dan berpotensi membuatnya down lagi. ❌ Resource Waste: Terlalu sering mencoba ulang request yang sama dalam waktu singkat bisa membuang-buang sumber daya klien dan server.
Exponential Backoff: Memberi Jeda yang Cerdas
Inilah mengapa kita membutuhkan exponential backoff. Alih-alih mencoba lagi secara instan atau dengan jeda waktu yang sama, exponential backoff meningkatkan jeda waktu antar percobaan ulang secara eksponensial.
Bagaimana cara kerjanya? Misalnya, Anda memulai dengan jeda 1 detik. Jika gagal lagi, jeda berikutnya adalah 2 detik. Gagal lagi, jeda 4 detik, lalu 8 detik, dan seterusnya. Ini memberikan waktu yang lebih lama bagi sistem yang bermasalah untuk pulih, sekaligus mengurangi beban pada server.
Rumus dasar: delay = base * multiplier^attempt
base: Waktu jeda awal (misal: 100ms, 1 detik).multiplier: Faktor pengali (misal: 2).attempt: Jumlah percobaan yang sudah dilakukan (mulai dari 0 atau 1).
Contoh dengan base = 100ms, multiplier = 2:
- Percobaan 1 (setelah gagal pertama):
100 * 2^0 = 100ms - Percobaan 2:
100 * 2^1 = 200ms - Percobaan 3:
100 * 2^2 = 400ms - Percobaan 4:
100 * 2^3 = 800ms
Jitter: Sentuhan Acak untuk Mencegah Kebersamaan
Meskipun exponential backoff mengurangi kemungkinan “thundering herd”, masih ada risiko jika banyak klien memulai retry pada saat yang bersamaan. Untuk mengatasi ini, kita menambahkan jitter — sedikit variasi acak pada jeda waktu.
Ada dua jenis jitter:
- Full Jitter: Mengambil jeda waktu acak antara 0 dan jeda yang dihitung oleh exponential backoff.
- Decorrelated Jitter: Menambahkan jitter ke jeda waktu sebelumnya, bukan dari 0.
Dengan jitter, klien-klien yang gagal tidak akan me-retry di waktu yang persis sama, menyebarkan beban ke server dan meningkatkan peluang keberhasilan.
3. Kapan Menggunakan Retry dan Kapan Tidak?
Penerapan retry dan exponential backoff tidak bisa sembarangan. Ada skenario di mana strategi ini sangat membantu, dan ada pula di mana justru bisa memperburuk keadaan.
✅ Kapan Menggunakan Retry?
-
Kegagalan Sementara (Transient Failures): Ini adalah kasus utama. Retry efektif untuk error yang bersifat sementara dan kemungkinan besar akan hilang setelah beberapa saat. Contohnya:
- Masalah Jaringan: Koneksi terputus sesaat, timeout, atau masalah DNS.
- Server Sedang Sibuk/Kelebihan Beban: Status
503 Service Unavailable,429 Too Many Requests, atau500 Internal Server Erroryang bukan karena bug permanen. - Koneksi Database Terputus Sementara: Di sisi backend, bisa menyebabkan error
500yang bersifat sementara. - Deadlock Database: Di sisi backend, bisa menyebabkan error sementara yang bisa diselesaikan dengan retry.
-
Request yang Idempotent: Ini adalah aturan emas. Request yang idempotent adalah request yang jika diulang berkali-kali, hasilnya akan sama dengan jika dieksekusi sekali saja.
- GET: Mengambil data. Mengambil data berkali-kali tidak akan mengubah state server.
- PUT: Memperbarui resource dengan payload yang lengkap. Jika diulang, resource akan tetap diperbarui ke state yang sama.
- DELETE: Menghapus resource. Menghapus resource yang sudah tidak ada tidak akan menyebabkan masalah tambahan (selain mungkin error
404 Not Founddi percobaan berikutnya, yang bisa diabaikan).
📌 Penting: Pastikan operasi backend Anda juga dirancang agar idempotent jika Anda mengimplementasikan retry untuk request PUT/DELETE.
❌ Kapan TIDAK Menggunakan Retry?
-
Kegagalan Permanen (Non-Transient Failures): Jika error menunjukkan masalah yang tidak akan hilang dengan retry, maka retry hanya akan membuang-buang waktu dan sumber daya. Contohnya:
- Error Validasi Input: Status
400 Bad Requestatau422 Unprocessable Entity. Input yang salah akan tetap salah meskipun diulang. - Autentikasi/Otorisasi Gagal: Status
401 Unauthorizedatau403 Forbidden. Kredensial yang salah tidak akan menjadi benar dengan retry. - Resource Tidak Ditemukan: Status
404 Not Found. Resource yang tidak ada akan tetap tidak ada. - Konflik Data: Status
409 Conflict. Biasanya terjadi karena state data yang tidak sesuai.
- Error Validasi Input: Status
-
Request yang Non-Idempotent: Ini adalah larangan paling penting. Jika request mengubah state server setiap kali dieksekusi, mengulangnya bisa menyebabkan efek samping yang tidak diinginkan.
- POST: Membuat resource baru. Jika Anda me-retry request POST tanpa mekanisme deduplikasi di backend, Anda bisa membuat resource duplikat. Bayangkan mengirim pesanan dua kali! 😱
💡 Tips: Jika Anda harus me-retry request POST, pastikan backend Anda memiliki mekanisme untuk mendeteksi dan mengabaikan request duplikat (misalnya, dengan menggunakan
Idempotency-Keydi header request). Namun, ini adalah kompleksitas tambahan yang sebaiknya dihindari jika memungkinkan di frontend.
Dengan memahami batasan ini, kita bisa menerapkan retry secara strategis untuk benar-benar meningkatkan keandalan aplikasi kita.
4. Implementasi Manual di JavaScript/TypeScript
Mari kita lihat bagaimana kita bisa mengimplementasikan retry dan exponential backoff secara manual menggunakan fetch API di JavaScript atau TypeScript. Ini akan memberi kita pemahaman mendalam tentang mekanismenya.
Pertama, kita akan membuat fungsi pembantu untuk menunda eksekusi (delay):
// delay.ts
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
Sekarang, mari kita buat fungsi fetchWithRetry utama:
// fetchWithRetry.ts
import { delay } from './delay';
interface RetryOptions {
maxRetries?: number;
baseDelayMs?: number; // Initial delay in milliseconds
multiplier?: number; // Factor to increase delay
shouldRetry?: (error: any) => boolean; // Custom retry condition
}
const DEFAULT_RETRY_OPTIONS: Required<RetryOptions> = {
maxRetries: 3,
baseDelayMs: 100, // 100ms initial delay
multiplier: 2,
shouldRetry: (error: any) => {
// Default: retry on network errors or 5xx status codes
if (error instanceof TypeError) { // Network error
return true;
}
if (error.response && error.response.status >= 500) {
return true;
}
return false;
}
};
async function fetchWithRetry<T>(
url: string,
options?: RequestInit,
retryOptions?: RetryOptions
): Promise<T> {
const mergedRetryOptions = { ...DEFAULT_RETRY_OPTIONS, ...retryOptions };
let currentAttempt = 0;
while (currentAttempt <= mergedRetryOptions.maxRetries) {
try {
const response = await fetch(url, options);
// Jika response bukan 2xx, kita bisa menganggapnya sebagai error
if (!response.ok) {
// Buat error object agar bisa ditangkap oleh catch block dan diperiksa statusnya
const error: any = new Error(`HTTP error! Status: ${response.status}`);
error.response = response; // Attach response for custom shouldRetry logic
throw error;
}
return await response.json() as T; // Atau response.text(), dll.
} catch (error: any) {
console.error(`Attempt ${currentAttempt + 1} failed for ${url}:`, error);
if (currentAttempt < mergedRetryOptions.maxRetries && mergedRetryOptions.shouldRetry(error)) {
const delayMs = mergedRetryOptions.baseDelayMs * Math.pow(mergedRetryOptions.multiplier, currentAttempt);
// Menambahkan jitter (full jitter)
const jitteredDelay = Math.random() * delayMs;
console.log(`Retrying in ${jitteredDelay.toFixed(0)}ms...`);
await delay(jitteredDelay);
currentAttempt++;
} else {
// Jika sudah mencapai maxRetries atau bukan jenis error yang di-retry
throw error;
}
}
}
// Seharusnya tidak tercapai jika error selalu dilemparkan
throw new Error("Max retries reached and operation failed permanently.");
}
Cara Penggunaan:
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
async function getPosts() {
try {
const posts = await fetchWithRetry<Post[]>(
'https://jsonplaceholder.typicode.com/posts/1',
{ method: 'GET' },
{
maxRetries: 5,
baseDelayMs: 200, // Mulai dari 200ms
multiplier: 2,
shouldRetry: (error) => {
// Contoh: hanya retry untuk 500 dan 503
if (error.response && (error.response.status === 500 || error.response.status === 503)) {
return true;
}
// Retry juga untuk error jaringan (TypeError)
if (error instanceof TypeError) {
return true;
}
return false;
}
}
);
console.log('Posts fetched successfully:', posts);
} catch (error) {
console.error('Failed to fetch posts after multiple retries:', error);
}
}
getPosts();
Penjelasan Kode:
delay(ms): Fungsi sederhana untuk membuat jeda asinkron.RetryOptions: Interface untuk mengkonfigurasi jumlah percobaan maksimum, jeda awal, pengali, dan kondisi kapan harus me-retry.DEFAULT_RETRY_OPTIONS: Nilai default agar lebih mudah digunakan. KondisishouldRetrydefault akan me-retry jika adaTypeError(indikasi error jaringan) atau status5xx.- Loop
while: Melakukan percobaan ulang hinggamaxRetriestercapai. try...catch: Menangkap error darifetch. Jikaresponse.okfalse, kita secara manual melemparkan error agar bisa ditangkap dan diproses.delayMs = mergedRetryOptions.baseDelayMs * Math.pow(mergedRetryOptions.multiplier, currentAttempt): Menghitung jeda dengan exponential backoff.jitteredDelay = Math.random() * delayMs: Menambahkan full jitter.
Ini adalah fondasi yang kokoh untuk memahami dan mengendalikan retry di aplikasi frontend Anda. Namun, untuk aplikasi yang lebih kompleks, menggunakan library mungkin lebih praktis.
5. Menggunakan Library Pihak Ketiga
Mengimplementasikan retry secara manual memang memberikan kontrol penuh, tetapi untuk skenario yang lebih kompleks atau untuk menghindari reinventing the wheel, menggunakan library pihak ketiga adalah pilihan yang bijak. Mereka seringkali datang dengan fitur tambahan seperti timeout, pembatalan, dan penanganan error yang lebih canggih.
1. Dengan Axios Interceptors
Jika Anda menggunakan Axios sebagai HTTP client, Anda bisa memanfaatkan fitur interceptors untuk menambahkan logic retry. Ada juga library seperti axios-retry yang mempermudah ini.
Pertama, instal axios-retry:
npm install axios axios-retry
# atau
yarn add axios axios-retry
Kemudian, konfigurasikan Axios:
import axios from 'axios';
import axiosRetry from 'axios-retry';
const api = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com',
timeout: 5000, // Timeout request 5 detik
});
axiosRetry(api, {
retries: 3, // Jumlah percobaan ulang
retryDelay: axiosRetry.exponentialDelay, // Menggunakan exponential backoff
retryCondition: (error) => {
// Hanya retry untuk 5xx errors atau network error
return axiosRetry.isNetworkError(error) || axiosRetry.isRetryableError(error);
},
onRetry: (retryCount, error, requestConfig) => {
console.log(`Retry attempt #${retryCount} for ${requestConfig.url}. Error: ${error.message}`);
},
});
async function getPostAxios() {
try {
const response = await api.get('/posts/1');
console.log('Post fetched successfully with Axios:', response.data);
} catch (error) {
console.error('Failed to fetch post with Axios after retries:', error);
}
}
getPostAxios();
// Contoh request yang mungkin gagal (misal: simulasi 500 error)
// Anda bisa mengarahkan ke endpoint yang tidak ada atau menggunakan mock server untuk simulasi error
async function demonstrateRetry() {
try {
const response = await api.get('/nonexistent-endpoint-to-fail');
console.log('This should not be reached:', response.data);
} catch (error) {
console.error('Failed to fetch after retries (expected for failure demo):', error.message);
}
}
// demonstrateRetry();
✅ Keuntungan axios-retry:
- Integrasi mulus dengan Axios.
- Sudah menyediakan fungsi
exponentialDelaydan helperisNetworkError,isRetryableError. - Mudah dikonfigurasi.
2. Library Khusus Retry (misalnya p-retry)
Untuk kasus yang lebih umum, di mana Anda mungkin tidak menggunakan Axios atau ingin mengulang operasi non-HTTP, library seperti p-retry sangat berguna. Ini adalah library berbasis Promise yang fleksibel.
Instal p-retry:
npm install p-retry
# atau
yarn add p-retry
Cara penggunaan:
import pRetry from 'p-retry';
async function unreliableOperation<T>(data: T): Promise<string> {
// Simulasikan operasi yang terkadang gagal
const random = Math.random();
if (random < 0.7) { // 70% kemungkinan gagal
console.log('Operation failed, will retry...');
throw new Error('Transient error: Operation failed!');
}
console.log('Operation succeeded!');
return `Success with data: ${data}`;
}
async function runWithPRetry() {
try {
const result = await pRetry(() => unreliableOperation('some data'), {
retries: 5, // Jumlah percobaan ulang (total 6 percobaan)
minTimeout: 100, // Jeda minimum (ms)
maxTimeout: 1000, // Jeda maksimum (ms)
factor: 2, // Faktor pengali untuk exponential backoff
onFailedAttempt: error => {
console.log(`Attempt ${error.attemptNumber} failed. Retrying...`);
},
});
console.log('Final result:', result);
} catch (error) {
console.error('Operation failed permanently after retries:', error);
}
}
runWithPRetry();
✅ Keuntungan p-retry:
- Fleksibel, bisa digunakan untuk mengulang Promise apa pun, tidak hanya HTTP request.
- Dukungan penuh untuk exponential backoff dan jitter.
- API yang bersih dan berbasis Promise.
Memilih library yang tepat akan sangat bergantung pada ekosistem dan kebutuhan proyek Anda. Untuk HTTP request dengan Axios, axios-retry adalah pilihan yang bagus. Untuk fleksibilitas yang lebih luas, p-retry adalah solusi yang solid.
6. Best Practices dan Pertimbangan Lanjutan
Implementasi retry dan exponential backoff yang efektif tidak hanya tentang kode, tetapi juga tentang bagaimana mengintegrasikannya ke dalam pengalaman pengguna dan sistem secara keseluruhan.
1. Feedback Visual untuk Pengguna 📌
Ketika aplikasi sedang mencoba ulang sebuah request, pengguna tidak boleh dibiarkan bingung.
- Loading Spinner/Indikator: Tampilkan indikator loading yang jelas saat retry sedang berlangsung.
- Pesan Status: Berikan pesan informatif seperti “Koneksi terputus, mencoba ulang…”, atau “Server sedang sibuk, harap tunggu…”. Ini mengurangi frustrasi pengguna.
- Waktu Jeda Visual: Jika jeda retry cukup lama, pertimbangkan untuk menunjukkan berapa lama lagi aplikasi akan mencoba.
❌ Hindari: Aplikasi yang membeku atau tidak responsif tanpa feedback saat retry sedang berjalan.
2. Implementasi Circuit Breaker Pattern ⚠️
Meskipun retry membantu mengatasi kegagalan sementara, ada kalanya server benar-benar down atau mengalami masalah serius untuk waktu yang lama. Jika kita terus me-retry request ke server yang sudah jelas-jelas down, kita hanya membuang-buang sumber daya dan memperburuk kondisi server (thundering herd).
Di sinilah Circuit Breaker Pattern masuk. Konsepnya mirip dengan circuit breaker listrik di rumah Anda: jika ada masalah (misal: korsleting), circuit breaker akan “trip” dan memutus aliran listrik untuk mencegah kerusakan lebih lanjut.
Dalam konteks aplikasi:
- Jika serangkaian request gagal berturut-turut, circuit breaker akan “membuka” (open).
- Ketika circuit breaker terbuka, semua request berikutnya akan gagal secara instan tanpa mencoba menghubungi server. Ini mencegah aplikasi membanjiri server yang sedang down.
- Setelah jeda waktu tertentu, circuit breaker akan beralih ke kondisi “setengah terbuka” (half-open) dan mengizinkan beberapa request percobaan untuk melihat apakah server sudah pulih.
- Jika request percobaan berhasil, circuit breaker akan “menutup” (closed) kembali. Jika gagal, akan kembali terbuka.
Meskipun lebih sering diimplementasikan di sisi backend, konsep ini bisa diterapkan di frontend untuk API-API kritikal. Library seperti opossum (untuk Node.js, tapi konsepnya universal) bisa menjadi referensi.
3. Logging dan Monitoring 💡
Untuk memahami seberapa sering retry terjadi dan mengapa, logging sangat penting.
- Catat setiap kali retry dilakukan: URL, jumlah percobaan, jenis error.
- Integrasikan dengan sistem monitoring (misal: Sentry, LogRocket) untuk mendapatkan visibilitas ke dalam error di produksi.
- Analisis data ini untuk mengidentifikasi pola kegagalan, seperti API mana yang paling sering gagal atau apakah ada waktu-waktu tertentu yang memiliki tingkat kegagalan tinggi.
4. Kontrol Pengguna (Tombol “Coba Lagi”) ✅
Meskipun retry otomatis itu bagus, terkadang pengguna ingin memiliki kontrol.
- Setelah
maxRetriestercapai dan request tetap gagal, tampilkan pesan error yang jelas dan berikan tombol “Coba Lagi” manual. Ini memberi pengguna pilihan dan rasa kontrol.
5. Pengujian Strategi Retry 🧪
Jangan lupakan pengujian!
- Unit Testing: Uji fungsi retry Anda secara terpisah, pastikan jeda waktu dihitung dengan benar dan kondisi retry berfungsi.
- Integration Testing / E2E Testing: Gunakan mock server atau alat seperti Cypress atau Playwright untuk mensimulasikan kegagalan jaringan atau server dan pastikan aplikasi Anda melakukan retry dengan benar dan memberikan feedback yang sesuai kepada pengguna.
Membangun aplikasi web yang tahan banting membutuhkan pemikiran yang matang tentang bagaimana menangani kegagalan. Dengan menerapkan best practices ini, Anda tidak hanya meningkatkan keandalan, tetapi juga kualitas pengalaman pengguna secara keseluruhan.
Kesimpulan
Selamat! Anda kini telah memahami pentingnya strategi retry dan exponential backoff untuk HTTP request di frontend. Kita telah melihat bagaimana mekanisme ini melindungi aplikasi dari kegagalan sementara, meningkatkan pengalaman pengguna, dan menjaga stabilitas sistem secara keseluruhan.
Dari implementasi manual dengan JavaScript/TypeScript yang memberi kita kontrol penuh, hingga pemanfaatan library populer seperti axios-retry dan p-retry yang menawarkan kemudahan dan fitur lengkap, Anda memiliki berbagai alat di gudang senjata Anda. Ingatlah selalu untuk mempertimbangkan kapan harus me-retry (error sementara, request idempotent) dan kapan tidak (error permanen, request non-idempotent).
Dengan menerapkan feedback visual yang cerdas, mempertimbangkan circuit breaker, melakukan logging yang efektif, memberikan kontrol kepada pengguna, dan menguji strategi Anda secara menyeluruh, Anda akan membangun aplikasi web yang tidak hanya fungsional, tetapi juga tangguh dan andal di tengah ketidakpastian dunia nyata. Teruslah berinovasi dan bangun web yang lebih baik!
🔗 Baca Juga
- Strategi Retry dan Exponential Backoff: Membangun Aplikasi yang Tahan Banting di Dunia Nyata
- Mengelola Error di Aplikasi Web Modern: Strategi Praktis untuk Kode yang Robust dan Mudah Didebug
- Membangun Aplikasi yang Tangguh: Strategi Graceful Degradation dan Fallback
- Membangun Sistem Tangguh: Mengimplementasikan Circuit Breaker Pattern dalam Aplikasi Anda