Kenapa ini penting
Aplikasi nyata jarang hanya menjalankan satu operasi asinkron. Dashboard keuangan mungkin perlu mengambil saldo, daftar transaksi, dan kurs mata uang sekaligus. Halaman produk memuat detail, ulasan, dan rekomendasi. Kamu perlu tahu cara menjalankan banyak Promise bersamaan dan memilih strategi yang tepat: tunggu semua? lanjut meskipun sebagian gagal? ambil yang tercepat?
Selain itu kamu akan memakai fetch, API standar untuk mengambil data dari server yang tersedia di browser dan di Node.js (versi 18 ke atas). Kamu juga belajar dua teknik ketahanan yang membedakan aplikasi amatir dan profesional: membatalkan request yang sudah tidak dibutuhkan atau terlalu lama (AbortController), dan mencoba ulang (retry) saat jaringan sedang tidak stabil, kondisi yang sangat umum di jaringan seluler Indonesia.
Konsep inti
Empat combinator Promise
| Method | Selesai ketika | Hasil | Gagal ketika |
|---|---|---|---|
Promise.all |
Semua fulfilled | Array value (urutan sesuai input) | Satu saja rejected (langsung) |
Promise.allSettled |
Semua settled | Array { status, value } / { status, reason } |
Tidak pernah reject |
Promise.race |
Yang pertama settled | Value/reason yang pertama | Jika yang pertama rejected |
Promise.any |
Yang pertama fulfilled | Value pertama yang berhasil | Semua rejected → AggregateError |
Analogi: all itu seperti menunggu semua teman datang sebelum makan (satu batal, acara batal). allSettled seperti absensi: tunggu sampai semua menjawab, hadir atau tidak. race seperti lomba lari: siapa pun yang pertama mencapai garis, termasuk yang jatuh. any seperti mencari tukang ojek: ambil yang pertama bersedia, yang menolak diabaikan.
Catatan: Promise yang "kalah" di race/any tidak otomatis dibatalkan, ia tetap berjalan di latar. Untuk benar-benar membatalkan, pakai AbortController.
fetch dengan async/await
const res = await fetch(url, { method, headers, body, signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
Dua hal yang sering mengejutkan:
fetchhanya reject untuk kegagalan jaringan (offline, DNS gagal, dibatalkan). Status HTTP 404 atau 500 tetap dianggap "berhasil" dan Promise-nya fulfilled! Kamu wajib mengecekres.ok(true untuk status 200-299).- Ada dua
await: satu untuk header response, satu lagi untuk membaca body (res.json(),res.text()).
Untuk mengirim data JSON: method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data).
AbortController
const controller = new AbortController();
fetch(url, { signal: controller.signal });
controller.abort(); // membatalkan → fetch reject dengan error bernama "AbortError"
Jalan pintas modern: AbortSignal.timeout(ms) membuat signal yang otomatis abort setelah ms (error bernama "TimeoutError"), dan AbortSignal.any([s1, s2]) menggabungkan beberapa signal. Keduanya tersedia di browser modern dan Node.js 20+.
Kegunaan: membatalkan pencarian lama saat pengguna mengetik kata baru, membatasi waktu tunggu, menghentikan request saat pengguna pindah halaman.
Retry sederhana dengan backoff
Kegagalan sementara (jaringan putus sebentar, server sibuk 503) sering berhasil jika dicoba lagi. Prinsip retry yang sehat:
- Batasi jumlah percobaan (misalnya 3).
- Beri jeda yang makin panjang (exponential backoff: 200 ms, 400 ms, 800 ms) agar tidak membanjiri server.
- Jangan retry error permanen seperti 400 (request salah) atau 404 (tidak ada). Retry hanya untuk error jaringan, timeout, 429, dan 5xx.
Contoh kode
const tunggu = (ms) => new Promise((r) => setTimeout(r, ms));
const sukses = (ms, v) => tunggu(ms).then(() => v);
const gagal = (ms, pesan) => tunggu(ms).then(() => { throw new Error(pesan); });
async function demoCombinator() {
// 1. Promise.all: semua harus berhasil
const [saldo, kurs] = await Promise.all([sukses(100, 2500000), sukses(50, 16250)]);
console.log("all:", saldo, kurs); // → all: 2500000 16250
try {
await Promise.all([sukses(100, "A"), gagal(50, "server transaksi down"), sukses(80, "C")]);
} catch (e) {
console.log("all gagal:", e.message); // → all gagal: server transaksi down
}
// 2. Promise.allSettled: lihat nasib masing-masing
const hasil = await Promise.allSettled([
sukses(50, "saldo OK"),
gagal(30, "kurs timeout"),
sukses(70, "transaksi OK"),
]);
hasil.forEach((h, i) =>
console.log(`allSettled[${i}]:`, h.status, h.status === "fulfilled" ? h.value : h.reason.message));
// → allSettled[0]: fulfilled saldo OK
// → allSettled[1]: rejected kurs timeout
// → allSettled[2]: fulfilled transaksi OK
// 3. Promise.race: yang pertama selesai, berhasil atau gagal
const tercepat = await Promise.race([sukses(120, "server Jakarta"), sukses(60, "server Singapura")]);
console.log("race:", tercepat); // → race: server Singapura
// 4. Promise.any: yang pertama BERHASIL
const cermin = await Promise.any([gagal(20, "mirror 1 mati"), sukses(90, "mirror 2"), sukses(60, "mirror 3")]);
console.log("any:", cermin); // → any: mirror 3
try {
await Promise.any([gagal(10, "x"), gagal(20, "y")]);
} catch (e) {
console.log("any gagal:", e.constructor.name, e.errors.map((x) => x.message)); // → any gagal: AggregateError [ 'x', 'y' ]
}
}
// 5. fetch dengan async/await + cek res.ok
async function ambilJSON(url, opsi = {}) {
const res = await fetch(url, opsi);
if (!res.ok) {
const err = new Error(`HTTP ${res.status} untuk ${url}`);
err.status = res.status;
throw err;
}
return res.json();
}
async function demoFetch() {
try {
const user = await ambilJSON("https://jsonplaceholder.typicode.com/users/1");
console.log("fetch:", user.name, "-", user.address.city); // → fetch: Leanne Graham - Gwenborough
await ambilJSON("https://jsonplaceholder.typicode.com/tidak-ada");
} catch (e) {
console.log("fetch error:", e.message); // → fetch error: HTTP 404 untuk https://jsonplaceholder.typicode.com/tidak-ada
}
// POST JSON
const baru = await ambilJSON("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "Catatan belanja", body: "Beras 5 kg", userId: 1 }),
});
console.log("POST id:", baru.id); // → POST id: 101
}
// 6. AbortController: batal manual & timeout
async function demoAbort() {
const controller = new AbortController();
setTimeout(() => controller.abort(), 5); // batalkan hampir seketika
try {
await fetch("https://jsonplaceholder.typicode.com/photos", { signal: controller.signal });
} catch (e) {
console.log("abort:", e.name); // → abort: AbortError
}
try {
await fetch("https://jsonplaceholder.typicode.com/photos", { signal: AbortSignal.timeout(1) });
} catch (e) {
console.log("timeout:", e.name); // → timeout: TimeoutError
}
}
// 7. Retry dengan exponential backoff
async function denganRetry(fn, { percobaan = 3, jedaAwal = 200 } = {}) {
for (let ke = 1; ; ke++) {
try {
return await fn(ke);
} catch (err) {
const permanen = err.status >= 400 && err.status < 500 && err.status !== 429;
if (permanen || ke >= percobaan) throw err;
const jeda = jedaAwal * 2 ** (ke - 1);
console.log(` percobaan ${ke} gagal (${err.message}), coba lagi dalam ${jeda} ms`);
await tunggu(jeda);
}
}
}
async function demoRetry() {
// server simulasi: gagal 2 kali lalu berhasil
let panggilan = 0;
const serverLabil = async () => {
panggilan++;
await tunggu(20);
if (panggilan < 3) {
const e = new Error("503 Service Unavailable");
e.status = 503;
throw e;
}
return { kursUSD: 16250 };
};
const data = await denganRetry(serverLabil);
console.log("retry berhasil:", data);
// → percobaan 1 gagal (503 Service Unavailable), coba lagi dalam 200 ms
// → percobaan 2 gagal (503 Service Unavailable), coba lagi dalam 400 ms
// → retry berhasil: { kursUSD: 16250 }
}
(async () => {
await demoCombinator();
await demoFetch().catch((e) => console.log("Jaringan tidak tersedia:", e.message));
await demoAbort();
await demoRetry();
})();
Contoh 5 dan 6 butuh koneksi internet (memakai API latihan gratis JSONPlaceholder). Contoh lainnya berjalan offline. Perhatikan denganRetry tidak mengulang error 4xx, karena request yang salah tidak akan menjadi benar hanya dengan diulang.
Kesalahan umum
- Mengira
fetchreject untuk 404/500. Tidak. Selalu cekres.okataures.status. - Memakai
Promise.allsaat kegagalan sebagian masih bisa diterima. Satu widget gagal membuat seluruh dashboard kosong. PakaiallSettleddan tampilkan bagian yang berhasil. - Lupa
await res.json().res.json()mengembalikan Promise; tanpaawaitkamu memegang Promise, bukan data. - Retry tanpa batas atau tanpa jeda. Bisa membanjiri server yang sedang kesusahan dan menguras baterai/kuota pengguna. Batasi dan gunakan backoff.
- Mengira
racemembatalkan Promise yang kalah. Request yang kalah tetap berjalan. Gabungkan denganAbortControllerjika memang ingin menghentikannya.
Latihan
- Mudah, Ambil
users/1,users/2, danusers/3dari JSONPlaceholder secara paralel denganPromise.all, lalu cetak nama-namanya. - Sedang, Tulis
ambilDenganTimeout(url, ms)memakaiAbortController+setTimeout(bukanAbortSignal.timeout) yang melemparError("Waktu habis")jika request lebih lama darims. Jangan lupa membersihkan timer. - Menantang, Tulis
muatDashboard()yang memanggil tiga sumber simulasi (saldo, transaksi, kurs; salah satunya gagal) denganallSettled, lalu mengembalikan object{ saldo, transaksi, kurs, peringatan: [...] }dengan nilainulluntuk sumber yang gagal.
Lihat solusi
// 1
async function tigaUser() {
const ids = [1, 2, 3];
const users = await Promise.all(
ids.map((id) => fetch(`https://jsonplaceholder.typicode.com/users/${id}`).then((r) => r.json())),
);
console.log(users.map((u) => u.name)); // → [ 'Leanne Graham', 'Ervin Howell', 'Clementine Bauch' ]
}
// 2
async function ambilDenganTimeout(url, ms) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);
try {
const res = await fetch(url, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
if (err.name === "AbortError") throw new Error("Waktu habis");
throw err;
} finally {
clearTimeout(timer); // bersihkan timer apa pun hasilnya
}
}
// 3
const jeda = (ms, v, gagal) => new Promise((res, rej) => setTimeout(() => (gagal ? rej(new Error(v)) : res(v)), ms));
async function muatDashboard() {
const sumber = ["saldo", "transaksi", "kurs"];
const hasil = await Promise.allSettled([
jeda(50, 2500000),
jeda(80, [{ ket: "Kopi", jumlah: -25000 }]),
jeda(30, "layanan kurs tidak merespons", true),
]);
const out = { peringatan: [] };
hasil.forEach((h, i) => {
out[sumber[i]] = h.status === "fulfilled" ? h.value : null;
if (h.status === "rejected") out.peringatan.push(`${sumber[i]}: ${h.reason.message}`);
});
return out;
}
(async () => {
await tigaUser().catch((e) => console.log("offline:", e.message));
await ambilDenganTimeout("https://jsonplaceholder.typicode.com/photos", 1)
.catch((e) => console.log(e.message)); // → Waktu habis
console.log(await muatDashboard());
// → { peringatan: [ 'kurs: layanan kurs tidak merespons' ], saldo: 2500000, transaksi: [ { ket: 'Kopi', jumlah: -25000 } ], kurs: null }
})();
Ringkasan
all(semua berhasil),allSettled(semua selesai),race(pertama selesai),any(pertama berhasil).fetchhanya reject untuk masalah jaringan; cekres.okdan jangan lupaawait res.json().AbortControllermembatalkan request;AbortSignal.timeout(ms)membatasi waktu tunggu.- Retry hanya untuk error sementara, dengan batas percobaan dan exponential backoff.
- Pilih combinator sesuai kebutuhan bisnis: apakah kegagalan sebagian boleh terjadi?