Fetch API Advanced: Abort, Retry, Timeout, dan Middleware
Daftar Isi
- Pengenalan
- Kenapa fetch Dasar Tidak Cukup
- Pembatalan Request dengan AbortController
- Menambahkan Timeout pada Request Fetch
- Strategi Retry yang Tidak Memperburuk Masalah
- Membangun Custom Fetch Middleware
- HTTP Client yang Composable
- Error Handling dan Response Parsing
- Kesalahan Umum di Kode Fetch Production
- Checklist Fetch yang Tangguh
- FAQ
- Kesimpulan
Pengenalan
Fetch API adalah cara standar untuk membuat HTTP request di browser modern dan Node.js. Untuk GET cepat atau submit form sederhana, API bawaan sudah cukup. Namun aplikasi production membutuhkan lebih: kemampuan membatalkan request yang sedang berjalan, menerapkan timeout, me-retry kegagalan sementara dengan aman, menambahkan header autentikasi secara konsisten, dan mencatat error tanpa menduplikasi logika di setiap call site.
Library seperti Axios dan Ky menyelesaikan sebagian masalah ini out of the box, tetapi memahami cara mengimplementasikannya dengan Fetch native memberi Kamu kontrol penuh, tanpa dependency tambahan, dan pola yang bisa diterapkan ke client HTTP mana pun.
Artikel ini membahas tiga kemampuan yang membedakan kode fetch prototipe dari HTTP client siap production: abort, retry, dan custom middleware.
Kenapa fetch Dasar Tidak Cukup
Pemanggilan fetch(url) polos memiliki beberapa celah yang terasa menyakitkan di aplikasi nyata:
- Tidak ada timeout bawaan. Server lambat atau hang dapat membuat request pending tanpa batas.
- Tidak ada retry otomatis. Kegagalan jaringan sementara harus ditangani manual di setiap call site.
- Tidak ada pembatalan request secara default. Navigasi keluar atau mengetik query baru tidak menghentikan request sebelumnya kecuali Kamu mengawirenya.
- Tidak ada lapisan middleware. Concern lintas modul seperti token auth, base URL, dan normalisasi error sering di-copy-paste ke banyak file.
- Tidak membedakan network error dan HTTP error. 404 dan kegagalan DNS keduanya bisa mengembalikan objek
Response—atau throw, tergantung jenis kegagalannya.
Ini bukan kelemahan Fetch. Fetch memang sengaja minimal. Pola dalam artikel ini mengisi celah tersebut tanpa meninggalkan API native.
Pembatalan Request dengan AbortController
AbortController adalah mekanisme standar untuk membatalkan request fetch. Bekerja di browser dan Node.js 18+.
Pembatalan dasar
const controller = new AbortController();
fetch('/api/users', { signal: controller.signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Request dibatalkan');
return;
}
throw error;
});
// Batalkan request
controller.abort();
Ketika abort() dipanggil, promise fetch reject dengan DOMException bernama AbortError. Selalu periksa nama ini, bukan membandingkan pesan error yang bervariasi antar environment.
Membatalkan saat component unmount
Di React, Svelte, atau Vue, ikat lifecycle controller ke component:
useEffect(() => {
const controller = new AbortController();
fetch('/api/dashboard', { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') setError(err);
});
return () => controller.abort();
}, []);
Ini mencegah update state pada component yang sudah unmount—sumber warning dan bug halus yang umum di SPA.
Membatalkan beberapa request sekaligus
Satu AbortController dapat membatalkan beberapa request terkait:
const controller = new AbortController();
const signal = controller.signal;
const usersPromise = fetch('/api/users', { signal });
const ordersPromise = fetch('/api/orders', { signal });
// Batalkan keduanya jika pengguna navigasi keluar
controller.abort();
Menggabungkan signal eksternal
Gunakan AbortSignal.any() (didukung luas di browser modern) untuk menggabungkan signal timeout dengan signal pembatalan pengguna:
const userController = new AbortController();
const timeoutSignal = AbortSignal.timeout(5000);
const signal = AbortSignal.any([
userController.signal,
timeoutSignal,
]);
fetch('/api/search?q=fetch', { signal });
Jika salah satu signal abort, fetch dibatalkan.
Menambahkan Timeout pada Request Fetch
Fetch tidak punya opsi timeout di init object, tetapi AbortSignal.timeout() memberi solusi yang bersih:
async function fetchWithTimeout(url, options = {}, timeoutMs = 8000) {
const response = await fetch(url, {
...options,
signal: AbortSignal.timeout(timeoutMs),
});
return response;
}
Menangani error timeout
Timeout abort juga menghasilkan AbortError. Bedakan dengan memeriksa apakah controller pengguna yang memicu abort:
try {
const response = await fetch('/api/data', {
signal: AbortSignal.timeout(3000),
});
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timeout setelah 3 detik');
}
throw error;
}
Timeout vs respons lambat
Timeout membatalkan tunggu di sisi client, bukan berarti pemrosesan di server ikut berhenti. Server mungkin tetap menyelesaikan pekerjaan setelah client menyerah. Untuk operasi mutasi (POST, PUT, DELETE), gabungkan timeout dengan idempotency key di server agar request retry tidak membuat efek samping ganda.
Strategi Retry yang Tidak Memperburuk Masalah
Retry meningkatkan reliabilitas untuk kegagalan sementara, tetapi retry sembarangan dapat memperbesar beban saat outage atau membuat record duplikat.
Kapan sebaiknya retry
Retry cocok untuk:
- network error (connection reset, kegagalan DNS)
- HTTP 408 (Request Timeout)
- HTTP 429 (Too Many Requests), perhatikan header
Retry-After - HTTP 502, 503, 504 (error gateway dan service unavailable)
Hindari retry untuk:
- HTTP 400, 401, 403, 404, 422 (client error—request-nya sendiri salah)
- HTTP 409 (conflict—retry bisa memperburuk situasi)
- POST non-idempotent tanpa perlindungan idempotency di server
Exponential backoff dengan jitter
async function fetchWithRetry(url, options = {}, config = {}) {
const {
maxRetries = 3,
baseDelayMs = 300,
maxDelayMs = 5000,
retryOn = [408, 429, 502, 503, 504],
} = config;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok || !retryOn.includes(response.status)) {
return response;
}
lastError = new Error(`HTTP ${response.status}`);
if (attempt === maxRetries) break;
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter
? Number(retryAfter) * 1000
: Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
const jitter = Math.random() * 100;
await new Promise(resolve => setTimeout(resolve, delay + jitter));
} catch (error) {
if (error.name === 'AbortError') throw error;
lastError = error;
if (attempt === maxRetries) break;
const delay = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
Jitter mencegah thundering herd ketika banyak client retry bersamaan setelah outage singkat.
Menghormati AbortSignal selama retry
Pass signal yang sama ke setiap attempt retry. Jika pengguna membatalkan atau timeout parent aktif, hentikan retry segera:
for (let attempt = 0; attempt <= maxRetries; attempt++) {
if (options.signal?.aborted) {
throw new DOMException('Aborted', 'AbortError');
}
// ... attempt fetch
}
Membangun Custom Fetch Middleware
Middleware membungkus fetch untuk menambah perilaku lintas modul tanpa mengubah setiap call site. Polanya mirip middleware Express atau interceptor Axios.
Signature middleware
Setiap middleware menerima URL, options, dan fungsi next yang memanggil layer berikutnya:
function createFetchPipeline(middlewares) {
const baseFetch = globalThis.fetch.bind(globalThis);
const dispatch = (index) => (url, options) => {
if (index >= middlewares.length) {
return baseFetch(url, options);
}
const middleware = middlewares[index];
return middleware(url, options, dispatch(index + 1));
};
return dispatch(0);
}
Contoh: middleware base URL
function withBaseUrl(baseUrl) {
return (url, options, next) => {
const fullUrl = url.startsWith('http') ? url : `${baseUrl}${url}`;
return next(fullUrl, options);
};
}
Contoh: middleware auth header
function withAuth(getToken) {
return async (url, options, next) => {
const token = await getToken();
const headers = new Headers(options.headers);
headers.set('Authorization', `Bearer ${token}`);
return next(url, { ...options, headers });
};
}
Contoh: middleware logging
function withLogging() {
return async (url, options, next) => {
const start = performance.now();
try {
const response = await next(url, options);
console.info(`[fetch] ${options.method ?? 'GET'} ${url} → ${response.status} (${Math.round(performance.now() - start)}ms)`);
return response;
} catch (error) {
console.error(`[fetch] ${options.method ?? 'GET'} ${url} → failed (${Math.round(performance.now() - start)}ms)`, error);
throw error;
}
};
}
Middleware dijalankan sesuai urutan registrasi. Tempatkan auth dan base URL di awal; logging dan retry biasanya membungkus fetch internal.
HTTP Client yang Composable
Gabungkan middleware, timeout, dan retry ke satu objek client:
const api = createFetchPipeline([
withBaseUrl('https://api.example.com'),
withAuth(() => localStorage.getItem('access_token')),
withLogging(),
withRetry({ maxRetries: 2 }),
]);
// Penggunaan
const response = await api('/v1/users/me', {
headers: { Accept: 'application/json' },
});
const user = await response.json();
Untuk retry middleware, bungkus next alih-alih fetch global:
function withRetry(config) {
return (url, options, next) => {
return fetchWithRetry(url, { ...options, fetchFn: () => next(url, options) }, config);
};
}
Alternatifnya, implementasikan retry di dalam middleware yang memanggil next dalam loop—menjaga logika retry dekat dengan pipeline, bukan tersebar di codebase.
Tips TypeScript
Definisikan tipe FetchFn bersama agar middleware tetap type-safe:
type FetchFn = (url: string | URL | Request, options?: RequestInit) => Promise<Response>;
type Middleware = (url: string, options: RequestInit, next: FetchFn) => Promise<Response>;
Error Handling dan Response Parsing
Fetch hanya reject saat network failure. Status HTTP error (4xx, 5xx) mengembalikan Response yang resolved. Tangani keduanya secara eksplisit:
async function parseResponse(response) {
if (!response.ok) {
const body = await response.text().catch(() => '');
throw new HttpError(response.status, body, response);
}
const contentType = response.headers.get('content-type') ?? '';
if (contentType.includes('application/json')) {
return response.json();
}
return response.text();
}
class HttpError extends Error {
constructor(status, body, response) {
super(`HTTP ${status}`);
this.name = 'HttpError';
this.status = status;
this.body = body;
this.response = response;
}
}
Memusatkan parsing mencegah setiap call site mengulang pengecekan if (!response.ok) dan mempermudah error boundary di framework UI.
Kesalahan Umum di Kode Fetch Production
Mengabaikan AbortError di catch block
Memperlakukan pembatalan seperti error nyata memicu toast error dan retry yang tidak perlu. Filter AbortError sejak awal.
Retry POST non-idempotent secara blind
Timeout pada request payment atau pembuatan order bisa berarti server sudah memprosesnya. Retry tanpa idempotency key dapat menduplikasi operasi.
Membuat AbortController baru per retry
Jika controller baru dibuat setiap attempt, signal pembatalan pengguna hilang. Reuse signal asli di semua attempt.
Tidak clone response sebelum membaca body
response.json() mengonsumsi body stream. Jika middleware dan caller keduanya butuh body, panggil response.clone() terlebih dahulu.
Hardcode jumlah retry untuk semua endpoint
GET read-heavy dapat toleransi retry lebih banyak daripada operasi write. Konfigurasikan kebijakan retry per route atau per instance middleware.
Lupa tentang CORS preflight
Header custom dari middleware auth memicu preflight OPTIONS. Pastikan server menanganinya, atau batch injeksi header untuk menghindari preflight tidak perlu pada request sederhana.
Checklist Fetch yang Tangguh
Pembatalan dan lifecycle
- Request berjalan lama menerima
AbortSignal. - Component membatalkan request in-flight saat unmount.
-
AbortErrorditangani terpisah dari kegagalan nyata.
Timeout
- Setiap request punya timeout yang jelas.
- Durasi timeout sesuai waktu respons server yang diharapkan.
- Request mutasi memakai idempotency key jika retry memungkinkan.
Retry
- Hanya kegagalan sementara yang di-retry.
- Backoff menyertakan jitter.
- Header
Retry-Afterdihormati untuk respons 429. - Loop retry memeriksa
signal.abortedsebelum setiap attempt.
Middleware dan struktur
- Auth, base URL, dan logging terpusat di middleware.
- HTTP error dinormalisasi ke tipe error konsisten.
- Response parsing menangani JSON, text, dan body kosong.
FAQ
Sebaiknya pakai Fetch atau library seperti Axios?
Fetch cukup untuk kebanyakan aplikasi jika Kamu menambahkan pola dalam artikel ini. Gunakan library jika butuh fitur seperti transform request/response otomatis, upload progress bawaan, atau ekosistem interceptor matang tanpa menulis pipeline sendiri.
Apakah AbortController bekerja di Node.js?
Ya, di Node.js 18 ke atas. API fetch, AbortController, dan AbortSignal.timeout() tersedia secara global.
Bisakah fetch dibatalkan setelah header respons diterima?
abort() menghentikan request di tahap apa pun—saat koneksi, menerima header, atau streaming body. Namun, server mungkin sudah menyelesaikan pemrosesan.
Berapa banyak retry yang dirasa cukup?
Untuk kebanyakan aplikasi client-side, dua atau tiga retry dengan exponential backoff adalah default yang wajar. Tambah retry hanya untuk operasi read idempotent di mana biaya kegagalan tinggi dan server mampu menangani beban ekstra.
Apakah aman me-retry POST request?
Hanya jika server mendukung idempotency key atau operasinya secara natural idempotent. Jika tidak, timeout diikuti retry dapat membuat record duplikat.
Bagaimana perbandingannya dengan Streams API untuk download besar?
Untuk download file besar, gabungkan fetch dengan ReadableStream dan AbortSignal untuk membatalkan di tengah stream. Pola abort dan middleware dalam artikel ini tetap berlaku; tambahkan progress tracking dengan membaca stream per chunk.
Kesimpulan
Fetch API memberi fondasi yang solid. Abort, retry, dan middleware mengubah fondasi itu menjadi client yang berperilaku benar di kondisi nyata—jaringan lambat, pengguna tidak sabar, dan server yang sesekali gagal.
Mulai dengan pembatalan untuk request yang terikat UI state. Tambahkan timeout berikutnya. Perkenalkan retry hanya untuk operasi idempotent atau terlindungi. Bungkus logika lintas modul di middleware agar call site tetap bersih.
Pola-pola ini kecil satu per satu, tetapi bersama-sama menghilangkan seluruh kelas bug production yang dibiarkan terbuka oleh kode fetch dasar.
Bug fetch apa yang paling sering Kamu temui di production? Timeout, abort yang hilang saat unmount, atau retry tanpa idempotency? Bagikan pengalamanmu di komentar.
Artikel Terkait
Lanjutkan membaca topik yang masih satu konteks.
Memory Leak JavaScript: Deteksi dan Fix dengan Chrome DevTools
Temukan dan fix memory leak JavaScript dengan Chrome DevTools, heap snapshot, cleanup listener, timer, closure, dan pola pencegahan.
Vue.js Tanpa NPM: Pakai Vue di Website Legacy
Tambahkan Vue.js ke website legacy tanpa npm lewat CDN, progressive enhancement, dan integrasi aman dengan halaman jQuery lama.
Django vs Flask vs FastAPI: Kapan Pakai Masing-Masing?
Bandingkan Django, Flask, dan FastAPI dari use case, async, admin, validasi, struktur project, dan kebutuhan backend Python nyata.
Tailwind CSS Advanced: Performa, Config, dan Utility
Optimasi Tailwind CSS dengan utility advanced, custom config, pola responsive, komponen reusable, ukuran build, dan tips performa.