Server Node sering harus menjalankan banyak operasi I/O paralel, misalnya mengambil beberapa sumber data sekaligus atau menulis banyak berkas dalam satu permintaan. Menjalankan operasi tersebut berurutan dengan await satu per satu menggandakan waktu total ke penjumlahan durasi, padahal I/O tidak saling bergantung. Kombinator promise seperti Promise.all dan Promise.allSettled menjadi alat untuk fan-out I/O dan menunggu hasilnya kembali dengan kebijakan penanganan kegagalan yang berbeda-beda. Topik ini menjelaskan kapan masing-masing kombinator dipakai dan bagaimana perilakunya terhadap promise yang rejected.
Promise.all untuk fan-out yang berhenti pada kegagalan pertama#
Promise.all menerima deret promise dan mengembalikan promise baru yang fulfilled setelah seluruh input fulfilled, dengan array hasil yang urutannya sama dengan urutan input. Apabila satu promise saja rejected, hasil keseluruhan langsung rejected dengan alasan yang sama, dan promise lain yang masih berjalan tidak dibatalkan meskipun hasilnya diabaikan. Berkas promise-all.js menjalankan dua skenario: semua sukses dan ada yang reject.
function ambil(nomor, delay, gagal = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (gagal) reject(new Error(`gagal #${nomor}`));
else resolve(`data #${nomor}`);
}, delay);
});
}
async function sukses() {
console.log("Promise.all (semua sukses):");
try {
const hasil = await Promise.all([
ambil(1, 80),
ambil(2, 40),
ambil(3, 120),
]);
console.log(" hasil:", hasil);
} catch (err) {
console.log(" tertangkap:", err.message);
}
}
async function gagal() {
console.log("Promise.all (ada yang reject):");
try {
const hasil = await Promise.all([
ambil(1, 80),
ambil(2, 40, true),
ambil(3, 120),
]);
console.log(" hasil:", hasil);
} catch (err) {
console.log(" tertangkap:", err.message);
}
}
(async () => {
await sukses();
await gagal();
})();Promise.all (semua sukses):
hasil: [ 'data #1', 'data #2', 'data #3' ]
Promise.all (ada yang reject):
tertangkap: gagal #2Pada kasus sukses, total waktu tunggu menyamai durasi terlama, yaitu 120 milidetik untuk ambil(3, 120), bukan penjumlahan 80 + 40 + 120 = 240 milidetik. Pada kasus gagal, reject dari ambil(2, 40) selesai lebih dahulu pada 40 milidetik dan langsung melempar pengecualian, sehingga hasil ambil(1, 80) maupun ambil(3, 120) yang masih berjalan tidak pernah dikumpulkan.
Perilaku short-circuit ini cocok ketika seluruh hasil bersifat wajib. Contoh nyata adalah pengambilan pengguna dan profil pengguna dalam satu permintaan HTTP: bila salah satu gagal, respons tidak dapat dirakit sehingga kegagalan harus diteruskan ke pemanggil.
Promise.allSettled untuk hasil parsial#
Promise.allSettled tidak pernah rejected. Kombinator ini menunggu seluruh promise settled, baik fulfilled maupun rejected, lalu mengembalikan array objek { status, value } atau { status, reason } sesuai keadaan tiap promise. Urutan hasil kembali sama dengan urutan input. Berkas promise-allsettled.js menjalankan tiga promise yang salah satunya reject.
function ambil(nomor, delay, gagal = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (gagal) reject(new Error(`gagal #${nomor}`));
else resolve(`data #${nomor}`);
}, delay);
});
}
async function jalankan() {
const hasil = await Promise.allSettled([
ambil(1, 80),
ambil(2, 40, true),
ambil(3, 120),
]);
console.log("Promise.allSettled:");
for (const r of hasil) {
if (r.status === "fulfilled") console.log(" fulfilled:", r.value);
else console.log(" rejected:", r.reason.message);
}
}
jalankan();Promise.allSettled:
fulfilled: data #1
rejected: gagal #2
fulfilled: data #3Total waktu tunggu tetap menyamai durasi terlama yaitu 120 milidetik, tetapi kegagalan ambil(2, 40) tidak membatalkan pengumpulan hasil lain. Pola ini cocok untuk operasi best-effort seperti pengambilan banyak sumber data opsional, pembersihan beberapa sumber daya paralel, atau pemanggilan banyak layanan eksternal yang hasilnya akan dirangkum sebagian.
Perbandingan pola fan-out#
Pemilihan kombinator bergantung pada kebijakan penanganan kegagalan dan kebutuhan hasil parsial.
| Kombinator | Hasil saat semua sukses | Hasil saat ada yang gagal | Cocok untuk |
|---|---|---|---|
Promise.all | Array nilai berurutan | Reject pertama, abaikan sisanya | Operasi wajib yang tidak boleh parsial |
Promise.allSettled | Array objek {status, value/reason} | Semua hasil tetap dikumpulkan | Operasi best-effort dengan hasil parsial |
Promise.race | Nilai promise pertama yang settled | Reject pertama yang settled | Timeout atau respons tercepat |
Promise.any | Nilai fulfilled pertama | AggregateError bila semua rejected | Redundansi dengan minimal satu sukses |
Pola timeout dengan Promise.race#
Promise.race mengembalikan promise yang settled mengikuti promise pertama yang selesai, baik fulfilled maupun rejected. Sifat ini menjadikannya kandidat untuk membatasi lama operasi I/O dengan cara mengombinasikan operasi utama dan timer dalam satu race. Bila timer lebih dahulu selesai, operasi utama dianggap gagal karena lewat batas waktu.
Pola klasik adalah membuat promise timer terpisah dan menggabungkannya dengan promise operasi: Promise.race([operasi, new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 500))]). Pendekatan ini memang memberi sinyal timeout ke pemanggil, tetapi operasi yang sedang berjalan tidak dibatalkan, sehingga beban kerja tetap berlanjut di latar dan sumber daya terbuang. Sejak Node 15, AbortController dan AbortSignal menjadi mekanisme pembatalan yang dipahami modul inti seperti fetch, fs.readFile, dan setTimeout versi promise, sehingga pembatalan sungguhan dapat diteruskan ke operasi yang sedang berjalan.
Kesalahan umum#
Menjalankan banyak await secara berurutan pada operasi yang seharusnya paralel. Pola const a = await ambil(1, 80); const b = await ambil(2, 40); menghasilkan total waktu 120 milidetik, padahal Promise.all cukup 80 milidetik. Selalu periksa apakah dependensi data benar-benar berurutan sebelum memakai await beruntun.
Mengira Promise.all membatalkan promise lain saat reject. Promise yang sudah berjalan tetap berjalan sampai selesai, hanya hasilnya yang tidak diharapkan. Bila efek samping seperti penulisan berkas atau panggilan API tetap berlangsung, pertimbangkan pembungkus dengan sinyal AbortController untuk membatalkan operasi yang tidak lagi diperlukan.
Ringkasan#
Promise.allmengumpulkan hasil paralel dan berhenti pada kegagalan pertama, cocok untuk operasi wajib.Promise.allSettledmenunggu seluruh promise selesai dan mengembalikan status tiap-tiap promise, cocok untuk hasil parsial.- Waktu total kedua kombinator menyamai durasi terlama, bukan penjumlahan durasi.
Promise.racedanPromise.anymelengkapi pola untuk kasus timeout dan redundansi.
Referensi#
- Promise.all() — MDN
— Perilaku
Promise.alltermasuk short-circuit pada reject. - Promise.allSettled() — MDN
— Bentuk objek hasil
{status, value/reason}dan urutan hasil. - Promise combinators — MDN
— Ringkasan
all,allSettled,race, danany.