Soal 1Saat kamu await fetch(url) dengan URL yang domain-nya tidak ada, error.name apa yang sampai ke catch?
Error Handling fetch dan Timeout — AbortController dan Retry
Membahas HTTP error yang tidak membuat fetch melempar exception, network error yang membuat fetch gagal, timeout dengan AbortController dan setTimeout, serta retry satu kali.
Dengan membaca response.ok, kamu bisa tahu saat server menjawab bahwa file-nya tidak ada. Tapi kalau koneksinya putus, baris await fetch sudah melempar exception sebelum kamu sempat membaca ok. Kalau response-nya lambat, kode terus menunggu dan tidak lanjut ke baris berikutnya.
Artikel ini membahas cara menangani tiap jenis kegagalan fetch sebagai exception, dan cara membatalkan request dengan AbortController.
Mengubah Response Gagal Menjadi Exception — throw new Error
Misalkan daftar order dimuat dari tiga halaman yang berbeda. Kalau loadJson mengembalikan null saat gagal, seperti di artikel sebelumnya, setiap halaman harus mengecek null. Kalau satu halaman saja lupa, baris berikutnya yang membaca value-nya akan error.
Pada HTTP error (request yang sampai ke server, tapi mendapat status tanda gagal seperti 403 atau 404), fetch tidak melempar exception. Kalau kamu throw new Error(...) saat ok bernilai false, pemanggil bisa menangani kegagalannya di satu tempat dengan try / catch.
// Kalau ok bernilai false, lempar exception yang menyertakan status
async function loadJson(path) {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${path}`);
}
return await response.json();
}
try {
const orders = await loadJson("fixtures/id/order.json"); // baris ini melempar exception
console.log(`Jumlah order: ${orders.length}`); // tidak pernah dijalankan
} catch (error) {
// 403 di console latihan, 404 atau sejenisnya di server lain
console.log(error.message); // HTTP 403: fixtures/id/order.json
}
Dengan throw, walaupun ada halaman yang lupa memasang catch, error yang tidak tertangkap itu tetap menyebut HTTP 403. Kamu tidak perlu menulis if untuk mengecek null di setiap halaman; cukup tentukan di mana kegagalannya ditangkap.
Membedakan Network Error — TypeError dan instanceof
Kalau sinyal hilang saat kamu sedang di luar, request tidak pernah sampai ke server dan tidak ada response yang kembali. Kegagalan ini masuk ke catch yang sama dengan HTTP error, jadi kalau dibiarkan begitu saja, kamu akan menampilkan pesan yang sama untuk “file-nya tidak ada” dan “tidak bisa terhubung”.
Pada network error (kegagalan saat tidak ada response yang datang, misalnya karena koneksi putus atau domain-nya tidak ada), Promise yang dikembalikan fetch menjadi rejected dengan TypeError. Kalau kamu mengecek instanceof TypeError di catch, kamu bisa membedakannya dari Error yang kamu lempar sendiri.
// loadJson sama dengan di bagian sebelumnya (melempar exception kalau ok bernilai false)
const urls = [
"fixtures/id/order.json", // sampai ke server, tapi file-nya tidak ada
"https://shop.invalid/orders.json", // domain .invalid tidak pernah terhubung ke mana pun
];
for (const url of urls) {
try {
await loadJson(url);
} catch (error) {
// Error yang kamu lempar sendiri, atau TypeError yang dilempar fetch?
const kind = error instanceof TypeError ? "Tidak bisa terhubung" : "Request gagal";
console.log(`${kind} / ${error.name}: ${error.message}`);
}
}
// Request gagal / Error: HTTP 403: fixtures/id/order.json
// Tidak bisa terhubung / TypeError: Failed to fetch
- Kedua kegagalan melempar exception di baris ini dan pindah ke
catch - Di
catch, cekerror instanceof TypeErroruntuk membedakannya
if (!response.ok)— untukorder.json, response datang danokbernilaifalsethrow new Error(...)—Erroryang kamu lempar sendiri
https://shop.invalid/…— tidak ada response yang datangTypeError: Failed to fetch— dilempar oleh fetch
TypeError dari fetch di bagian dalam langsung keluar dari loadJson tanpa sempat sampai ke if-nya. Setiap halaman butuh pesannya sendiri, jadi loadJson cukup melempar exception, dan pengecekan jenisnya diserahkan ke catch di pemanggil, yang menerima kedua jenis kegagalan.
Pesan Network Error Berbeda di Tiap Browser
message dari TypeError yang dilempar fetch berbeda di tiap browser: Failed to fetch di Chrome dan Load failed di Safari. Kalau kamu mengeceknya dengan mencocokkan teks pesan, browser lain akan masuk ke cabang yang salah. Cek jenisnya dengan instanceof TypeError.
Menghentikan Request yang Terlalu Lama — AbortController
Server yang sedang sibuk bisa butuh puluhan detik untuk merespons. fetch tidak punya opsi untuk mengatur batas waktu tunggu, jadi baris await fetch tidak lanjut sampai response datang, dan layar terlihat macet di tahap loading. Kode di bagian ini memakai slowFetch, pengganti fetch yang butuh 300ms untuk merespons.
Kalau kamu membuat AbortController (object untuk membatalkan fetch yang sedang berjalan) lalu memberikannya sebagai fetch(url, { signal: controller.signal }), memanggil abort() akan membuat request gagal dengan AbortError.
Untuk membatalkan panggilan yang sudah dijadwalkan, berikan timer ID (angka yang menandai panggilan terjadwal itu) yang dikembalikan setTimeout ke clearTimeout.
// slowFetch adalah pengganti fetch yang dideklarasikan di console Latihan 3; butuh 300ms untuk merespons dan dipakai persis seperti fetch
const controller = new AbortController();
// Jadwalkan abort() agar jalan setelah 100ms, lalu simpan timer ID-nya
const timerId = setTimeout(() => controller.abort(), 100);
try {
// Sama seperti fetch, berikan signal di argument kedua
const response = await slowFetch("fixtures/id/orders.json", { signal: controller.signal });
console.log(response.ok); // response butuh 300ms, jadi baris ini tidak pernah dijalankan
} catch (error) {
console.log(error.name); // AbortError
} finally {
clearTimeout(timerId); // batalkan jadwalnya, berjaga-jaga kalau response datang lebih dulu
}
Dengan menaruh clearTimeout di finally, tidak ada panggilan terjadwal yang tertinggal, baik request-nya berhasil maupun gagal. Kalau abort() jalan sebelum body selesai dibaca, json() juga gagal dengan AbortError, jadi taruh return await response.json() di dalam try juga, dan lanjut ke finally hanya setelah body selesai dibaca.
Mencoba Sekali Lagi — Retry dan return await
Di ponsel yang sedang dipakai di kereta, koneksi bisa putus sebentar dan request-nya gagal, padahal kalau langsung dicoba lagi sering kali berhasil. Daripada meminta pengguna me-reload halaman setiap kali, kode kamu bisa mencoba ulang secara otomatis supaya data di layar tetap tampil.
Untuk melakukan retry (mengirim ulang request yang gagal ke URL yang sama), panggil function-nya sekali lagi di dalam catch. Tanpa batas jumlah percobaan, kode akan terus mengirim request selama koneksinya putus, jadi kalau percobaan kedua juga gagal, error-nya langsung diteruskan ke pemanggil.
// loadJson sama dengan di bagian pertama. Retry 1 kali, hanya untuk network error dan timeout
async function loadWithRetry(path) {
try {
return await loadJson(path);
} catch (error) {
const retryable = error instanceof TypeError || error.name === "AbortError";
if (!retryable) {
throw error; // dikirim ulang pun hasilnya tetap 403 atau 404
}
console.log(`Mencoba ulang setelah ${error.name}`); // Mencoba ulang setelah TypeError
return await loadJson(path); // kegagalan kedua langsung diteruskan ke pemanggil
}
}
try {
await loadWithRetry("https://shop.invalid/orders.json");
} catch (error) {
console.log(`Gagal dua kali: ${error.name}`); // Gagal dua kali: TypeError
}
HTTP error, seperti untuk fixtures/id/order.json, dilempar ulang di catch yang pertama, jadi langsung diteruskan ke pemanggil tanpa panggilan kedua. Tabel di bawah menunjukkan cara mengenali tiap jenis kegagalan di catch dan apakah perlu di-retry.
| Kegagalan | Cek di catch | Retry? |
|---|---|---|
| Network error | error instanceof TypeError | Sekali (koneksi bisa pulih) |
| Timeout (abort()) | error.name === "AbortError" | Sekali (server bisa lebih lengang) |
| HTTP error (403, 404) | Selain di atas | Tidak (response-nya sama) |
return Tanpa await Melewatkan catch
Kalau kamu menulis try { return loadJson(path); } tanpa await, function-nya langsung keluar dari try begitu Promise-nya dikembalikan. Walaupun Promise itu nanti rejected, catch tidak dijalankan, dan kegagalannya langsung diteruskan ke pemanggil. Tulis return await loadJson(path);.
Cek Pemahaman
Jawab setiap pertanyaan satu per satu.
Soal 2Kamu menjadwalkan abort() untuk 100ms dari sekarang dan memberikan signal ke request yang butuh 300ms untuk merespons. Apa yang terjadi?
Soal 3Kegagalan mana yang dilempar ulang oleh loadWithRetry di artikel ini tanpa di-retry?