Belajar dengan membaca secara berurutan

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
}
throw Membawa Penyebabnya ke catch
Kalau ok false,return nullordersbernilai nullBacaorders.lengthTypeError, info403 hilangKalau ok false,throwBaris awaitloadJson melemparLewati sisa baris,lompat ke catcherror.message:HTTP 403: …
Kalau kamu mengembalikan null, error lain muncul di baris orders.length. Kalau kamu throw, pesan yang menyertakan status sampai ke catch.

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.

Di halaman status pengiriman, muat daftar order. Kerangka function loadOrders sudah tersedia, dan kedua path-nya tertulis di bagian atas kode.

① Kalau response menandakan gagal, lempar exception dengan pesan “Gagal memuat order: ” diikuti path-nya.

② Kalau berhasil, ubah body menjadi array lalu kembalikan.

③ Muat dengan path yang benar, lalu tampilkan total semua order.

④ Muat dengan path yang salah ketik, dan kalau gagal, tampilkan pesan exception-nya.

(Kalau berjalan dengan benar, penjelasan akan muncul.)

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

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
Asal Tiap Jenis Kegagalan
try di pemanggil — baris await loadJson(url)
  • Kedua kegagalan melempar exception di baris ini dan pindah ke catch
  • Di catch, cek error instanceof TypeError untuk membedakannya
Di dalam loadJson
  • if (!response.ok) — untuk order.json, response datang dan ok bernilai false
  • throw new Error(...)Error yang kamu lempar sendiri
Di dalam await fetch(url)
  • https://shop.invalid/… — tidak ada response yang datang
  • TypeError: Failed to fetchdilempar oleh fetch
HTTP error dilempar oleh loadJson, sedangkan network error dilempar oleh fetch. Keduanya sampai ke catch yang sama, jadi bedakan dari jenisnya.

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.

Di halaman stok barang, tampilkan pesan yang berbeda untuk tiap jenis kegagalan. loadJson dan urls (tiga URL) sudah dideklarasikan.

① Di showStock, muat daftar produk lalu tampilkan baris seperti “4 produk”.

② Kalau request tidak pernah sampai ke server, tampilkan “Server tidak bisa dihubungi”.

③ Untuk kegagalan lainnya, tampilkan “Daftar produk gagal dimuat”.

④ Berikan setiap URL di urls ke showStock secara berurutan.

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

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
}
Mana yang Lebih Dulu: Timer atau Response
Batas 100msresponse di 300msDi 100ms,abort() jalanfetch gagaldengan AbortErrorcatch menanganitimeoutBatas 3000msresponse di 300msDi 300ms,response datangclearTimeoutdi finallyabort() tidakpernah dipanggil
Kalau response lebih lambat dari batas waktu, abort() jalan lebih dulu dan request-nya gagal. Kalau response datang tepat waktu, batalkan abort() yang sudah dijadwalkan.

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.

Di halaman detail paket langganan, beri batas waktu untuk memuat daftar member. slowFetch, pengganti API yang lambat, sudah dideklarasikan.

① Di dalam loadUsers, jadwalkan pembatalan request setelah batas waktunya lewat.

② Pakai slowFetch untuk mengambil daftar member dengan cara yang bisa dibatalkan, lalu kembalikan array dari body-nya.

③ Batalkan panggilan yang sudah dijadwalkan, baik request-nya berhasil maupun gagal.

④ Dengan batas 3000ms, tampilkan jumlah member; dengan batas 100ms, tampilkan nama error-nya.

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

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
}
Retry Hanya Sekali
loadWithRetry(path) dipanggilPercobaan ke-1berhasilPercobaan ke-1TypeErrorPercobaan ke-1TypeErrorTidak adapanggilan ke-2Percobaan ke-2berhasilPercobaan ke-2juga TypeErrorArray daripercobaan ke-1dikembalikanLog retry, lalukembalikan arrayTidak ada ke-3,error dilempar
Kalau percobaan pertama berhasil, tidak ada panggilan kedua. Kalau percobaan kedua juga gagal, tidak ada percobaan ketiga; exception-nya diteruskan ke pemanggil.

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.

KegagalanCek di catchRetry?
Network errorerror instanceof TypeErrorSekali (koneksi bisa pulih)
Timeout (abort())error.name === "AbortError"Sekali (server bisa lebih lengang)
HTTP error (403, 404)Selain di atasTidak (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);.

Muat daftar member dari API yang lambat. slowFetch dan loadWithin sudah dideklarasikan.

① Di loadWithRetry, muat data dengan batas waktu limitMs lalu kembalikan hasilnya.

② Hanya kalau terjadi timeout, tampilkan “Waktu habis, mencoba ulang” lalu muat sekali lagi dengan batas 3000ms.

③ Muat daftar member dengan loadWithRetry lalu tampilkan baris seperti “4 member”.

④ Muat juga path yang salah, lalu tampilkan “Berhenti mencoba: ” diikuti pesan exception-nya.

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output
QUIZ

Cek Pemahaman

Jawab setiap pertanyaan satu per satu.

Soal 1Saat kamu await fetch(url) dengan URL yang domain-nya tidak ada, error.name apa yang sampai ke catch?

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?