Materi sebelumnya menunjukkan bahwa await membuat langkah asynchronous berjalan berurutan seperti kode sinkron. Kebijakan itu tepat saat setiap langkah bergantung pada hasil langkah sebelumnya, tetapi berubah menjadi pemborosan ketika tugas-tugas saling bebas: mengambil tiga sumber data secara bergantian dengan await memakan jumlah waktu yang merupakan penjumlahan ketiganya, padahal jaringan sanggup memproses ketiganya sekaligus. Chapter ini membahas empat combinator Promise — all, allSettled, any, dan race — yang mengatur banyak tugas paralel dengan kebijakan penggabungan hasil yang berbeda-beda.

Promise.all untuk Hasil Lengkap #

Promise.all() menerima sebuah array berisi Promise lalu mengembalikan satu Promise gabungan. Semua tugas dimulai hampir bersamaan; Promise.all baru selesai ketika seluruhnya sukses, dan hasilnya berupa array dengan urutan yang mengikuti urutan input, bukan urutan penyelesaian. Bila satu saja tugas ditolak, seluruh gabungan langsung ditolak dengan alasan penolakan pertama yang terjadi:

js
function tunda(ms, nilai) {
  return new Promise((resolve) => setTimeout(() => resolve(nilai), ms));
}

function tolak(ms, pesan) {
  return new Promise((_, reject) => setTimeout(() => reject(new Error(pesan)), ms));
}

const hasil = await Promise.all([
  tunda(30, "sumber A"),
  tunda(10, "sumber B"),
  tunda(20, "sumber C"),
]);

console.log(hasil);

try {
  await Promise.all([
    tunda(30, "sumber A"),
    tolak(10, "sumber B tidak siap"),
  ]);
} catch (e) {
  console.log(`Promise.all berhenti: ${e.message}`);
}

Output yang Dihasilkan:

text
[ 'sumber A', 'sumber B', 'sumber C' ]
Promise.all berhenti: sumber B tidak siap

Mari kita bedah hasilnya:

  1. Baris 10-14: tiga Promise berjalan paralel dengan jeda tetap 30, 10, dan 20 milidetik. Meskipun sumber B selesai paling cepat, array hasil tetap terurut seperti input — sumber A tetap di posisi pertama.
  2. Baris 20-24: pada pemanggilan kedua, sumber B ditolak setelah 10 milidetik. await melempar Error tersebut ke catch, dan pesan penolakan B yang tercetak — bukan hasil sumber A yang sebenarnya sudah berjalan.
  3. Kegagalan satu anggota membuat Promise.all berhenti lebih awal, tetapi tugas lain yang sedang berjalan tidak dibatalkan — hasilnya hanya diabaikan.

Promise.allSettled untuk Laporan Menyeluruh #

Kadang kita justru ingin semua tugas selesai apa pun hasilnya — misalnya mengambil data dari banyak layanan lalu melaporkan mana yang sehat dan mana yang bermasalah. Promise.allSettled() tidak pernah menolak; ia menunggu semua tugas selesai dan mengembalikan array status { status, value } atau { status, reason }:

js
function tunda(ms, nilai) {
  return new Promise((resolve) => setTimeout(() => resolve(nilai), ms));
}

function tolak(ms, pesan) {
  return new Promise((_, reject) => setTimeout(() => reject(new Error(pesan)), ms));
}

const laporan = await Promise.allSettled([
  tunda(30, "sumber A"),
  tolak(10, "sumber B tidak siap"),
  tunda(20, "sumber C"),
]);

for (const r of laporan) {
  if (r.status === "fulfilled") {
    console.log(`sukses: ${r.value}`);
  } else {
    console.log(`gagal : ${r.reason.message}`);
  }
}

Output yang Dihasilkan:

text
sukses: sumber A
gagal : sumber B tidak siap
sukses: sumber C

Program mencetak laporan lengkap ketiga sumber meskipun sumber B gagal. Properti r.value hanya ada ketika status bernilai "fulfilled"; sebaliknya r.reason membawa object error ketika "rejected". Pola pemeriksaan status seperti di atas adalah cara standar mengonsumsi hasil allSettled.

Promise.any dan Promise.race #

Dua combinator terakhir hanya peduli pada tugas yang paling dulu selesai. Promise.any() menunggu sukses pertama dan mengabaikan penolakan — cocok untuk pola replika: coba beberapa sumber, pakai yang tercepat. Promise.race() menunggu penyelesaian pertama apa pun bentuknya — sukses atau gagal — yang menjadikannya dasar pola batas waktu:

js
function tunda(ms, nilai) {
  return new Promise((resolve) => setTimeout(() => resolve(nilai), ms));
}

function tolak(ms, pesan) {
  return new Promise((_, reject) => setTimeout(() => reject(new Error(pesan)), ms));
}

const tercepatSukses = await Promise.any([
  tolak(10, "replika A gagal"),
  tunda(20, "replika B"),
]);

console.log(`Promise.any: ${tercepatSukses}`);

try {
  await Promise.race([
    tolak(10, "batas waktu terlampaui"),
    tunda(30, "data lengkap"),
  ]);
} catch (e) {
  console.log(`Promise.race ditolak: ${e.message}`);
}

try {
  await Promise.any([
    tolak(10, "replika A gagal"),
    tolak(20, "replika B gagal"),
  ]);
} catch (e) {
  console.log(`${e.name}: ${e.errors.map((x) => x.message).join(" | ")}`);
}

Output yang Dihasilkan:

text
Promise.any: replika B
Promise.race ditolak: batas waktu terlampaui
AggregateError: replika A gagal | replika B gagal

Tiga perilaku berbeda tampak pada hasilnya: Promise.any tetap sukses lewat replika B meski replika A gagal; Promise.race kalah cepat dari “batas waktu” sehingga penolakan batas waktu yang menang; dan ketika semua anggota Promise.any gagal, ia melempar AggregateError yang properti errors-nya menampung seluruh alasan kegagalan.

Perbandingan ringkas keempat combinator:

CombinatorSelesai ketikaHasil suksesBila ada rejectionKapan dipakai
Promise.allSemua suksesArray hasil terurutLangsung ditolak (alasan pertama)Semua data wajib ada
Promise.allSettledSemua selesaiArray statusTidak pernah ditolakLaporan/kondisi semua sumber
Promise.anySukses pertamaNilai pemenangAggregateError bila semua gagalReplika, sumber tercepat
Promise.raceSelesai pertamaNilai/penolakan pemenangMengikuti penyelesaian pertamaBatas waktu

Kita kini punya perangkat lengkap untuk mengatur apa yang berjalan paralel dan bagaimana hasilnya digabungkan. Lapisan kontrol berikutnya lebih halus: menentukan kapan persisnya sebuah callback berjalan relatif terhadap event loop — di antara microtask, fase check, atau putaran berikutnya. Chapter process.nextTick dan setImmediate membedah alat-alat tersebut.