FRONTEND WEB-DEVELOPMENT ERROR-HANDLING RESILIENCE NETWORK JAVASCRIPT TYPESCRIPT USER-EXPERIENCE RELIABILITY API-INTEGRATION BEST-PRACTICES CLIENT-SIDE HTTP PERFORMANCE

Mengelola Retry dan Exponential Backoff untuk HTTP Request di Frontend: Membangun Aplikasi Web yang Tahan Banting

⏱️ 17 menit baca
👨‍💻

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

Contoh dengan base = 100ms, multiplier = 2:

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:

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?

  1. 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, atau 500 Internal Server Error yang bukan karena bug permanen.
    • Koneksi Database Terputus Sementara: Di sisi backend, bisa menyebabkan error 500 yang bersifat sementara.
    • Deadlock Database: Di sisi backend, bisa menyebabkan error sementara yang bisa diselesaikan dengan retry.
  2. 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 Found di 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?

  1. 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 Request atau 422 Unprocessable Entity. Input yang salah akan tetap salah meskipun diulang.
    • Autentikasi/Otorisasi Gagal: Status 401 Unauthorized atau 403 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.
  2. 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-Key di 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:

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:

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:

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.

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:

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.

4. Kontrol Pengguna (Tombol “Coba Lagi”) ✅

Meskipun retry otomatis itu bagus, terkadang pengguna ingin memiliki kontrol.

5. Pengujian Strategi Retry 🧪

Jangan lupakan pengujian!

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