1. Cara Kerja fetch() dan Mengapa Butuh Dua Kali await
Fetch API adalah standar modern di peramban maupun runtime server (Node.js 18+) untuk melakukan permintaan jaringan HTTP berbasis Promise.
Hal pertama yang sering membingungkan pemula adalah mengapa mengambil data dengan fetch() membutuhkan dua kali await:
async function ambilDaftarKelas() {
// await pertama: Menunggu HEADER respons HTTP tiba dari server (status 200, Content-Type, dsb.)
const respons = await fetch("https://kodeloka.asadinedu.com/data/catalog.json");
// Wajib periksa apakah kode status berada di rentang sukses 200–299!
if (!respons.ok) {
throw new Error(`Permintaan gagal dengan status HTTP ${respons.status}`);
}
// await kedua: Menunggu aliran isi BODY selesai diunduh dan diurai sebagai objek JSON
const data = await respons.json();
return data;
}
Jebakan Paling Umum: fetch() TIDAK Melempar Galat pada Status 404 atau 500!
Berbeda dengan beberapa pustaka pihak ketiga, fetch() hanya akan menolak (reject) Promise-nya apabila terjadi kegagalan jaringan tingkat bawah (misalnya perangkat pengguna terputus dari internet, resolusi DNS gagal, atau permintaan dibatalkan).
Jika server membalas dengan kode status 404 Not Found, 401 Unauthorized, atau 500 Internal Server Error, fetch() menganggap komunikasi jaringan berhasil dan **tetap me-resolve Promise menjadi objek Response (dengan respons.ok bernilai false)! Oleh karena itu, selalu tulis pemeriksaan if (!respons.ok)** sebelum memanggil await respons.json().
2. Mengirim Data dengan POST, Header, dan FormData
A. Mengirim Payload JSON (Content-Type: application/json)
Ketika mengirim objek JavaScript sebagai JSON melalui metode POST, PUT, atau PATCH, Anda harus mengubah objek menjadi string menggunakan JSON.stringify() dan mencantumkan header Content-Type: application/json:
async function simpanProgresModul(idModul, skorKuis) {
const respons = await fetch("/api/progres", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({
modul_id: idModul,
skor: skorKuis,
waktu: new Date().toISOString()
})
});
if (!respons.ok) {
const rincianGalat = await respons.json().catch(() => null);
throw new Error(rincianGalat?.pesan ?? `Gagal menyimpan (HTTP ${respons.status})`);
}
return await respons.json();
}
B. Mengirim Formulir atau Berkas dengan FormData
Jika Anda mengambil data langsung dari elemen <form> di HTML (termasuk unggahan berkas <input type="file">), gunakan objek FormData:
async function kirimFormulirPendaftaran(elemenForm) {
const dataForm = new FormData(elemenForm);
const respons = await fetch("/api/pendaftaran", {
method: "POST",
// PENTING: JANGAN mengatur header Content-Type secara manual saat mengirim FormData!
// Peramban otomatis mengatur Content-Type: multipart/form-data beserta string 'boundary' uniknya.
body: dataForm
});
if (!respons.ok) throw new Error(`Gagal mengirim formulir: ${respons.status}`);
return await respons.json();
}
3. Merangkai Parameter Query Secara Aman dengan URL dan URLSearchParams
Jangan pernah menyambung parameter URL secara manual dengan string template seperti ` /api/cari?q=${kataKunci} ! Jika pengguna mengetik kata kunci "C++ & Go", karakter & dan +` akan merusak struktur parameter URL Anda.
Gunakan antarmuka standar URL dan URLSearchParams yang otomatis mengodekan karakter khusus (URL-encoding) dengan aman:
function buatUrlPencarian(kataKunci, bidang = "semua", halaman = 1) {
const url = new URL("https://kodeloka.asadinedu.com/api/kelas");
url.searchParams.set("q", kataKunci);
url.searchParams.set("bidang", bidang);
url.searchParams.set("page", String(halaman));
return url.toString();
}
console.log(buatUrlPencarian("Git & CI/CD", "infra", 1));
// Keluaran aman: "https://kodeloka.asadinedu.com/api/kelas?q=Git+%26+CI%2FCD&bidang=infra&page=1"
4. Membatalkan Permintaan dan Mengatur Batas Waktu (AbortController & AbortSignal.timeout)
Tanpa batas waktu (timeout), permintaan fetch() ke server yang sedang macet dapat menggantung selama lebih dari satu menit. Selain itu, pada fitur pencarian langsung (live search), ketika pengguna mengetik huruf baru, permintaan pencarian dari huruf sebelumnya harus dibatalkan agar balasannya yang terlambat tidak menimpa hasil terbaru (race condition).
A. Batas Waktu Otomatis Satu Baris dengan AbortSignal.timeout()
Sejak April 2024 (status Baseline Newly Available), Anda tidak perlu lagi membuat setTimeout manual untuk membatalkan fetch(). Cukup pasang signal: AbortSignal.timeout(milidetik):
async function ambilDataDenganBatasWaktu(url, batasMs = 5000) {
try {
const respons = await fetch(url, {
signal: AbortSignal.timeout(batasMs)
});
if (!respons.ok) throw new Error(`Status HTTP: ${respons.status}`);
return await respons.json();
} catch (err) {
if (err.name === "TimeoutError") {
throw new Error(`Permintaan ke server melebihi batas waktu ${batasMs} ms.`);
}
if (err.name === "AbortError") {
throw new Error("Permintaan dibatalkan oleh pengguna.");
}
throw err;
}
}
B. Pembatalan Manual untuk Live Search dengan AbortController
let pengendaliAktif = null;
async function cariMateriLangsung(kataKunci) {
// Jika masih ada permintaan sebelumnya yang belum selesai, batalkan sekarang!
if (pengendaliAktif) {
pengendaliAktif.abort();
}
pengendaliAktif = new AbortController();
const url = new URL("/api/materi", window.location.origin);
url.searchParams.set("q", kataKunci);
const respons = await fetch(url, {
signal: pengendaliAktif.signal
});
if (!respons.ok) throw new Error(`HTTP ${respons.status}`);
return await respons.json();
}
Latihan Praktik: Klien HTTP Tangguh dengan URLSearchParams dan Timeout
Buatlah berkas klien-api.js yang mengekspor fungsi ambilModul(filterObjek, timeoutMs = 4000):
- Gunakan
new URLSearchParams(filterObjek)untuk merangkai parameter query dengan aman. - Panggil
fetch()dengansignal: AbortSignal.timeout(timeoutMs)dan headerAccept: application/json. - Periksa
respons.okdan tangani pembedaan antaraTimeoutErrordan galat HTTP biasa secara rapi.