Javascript Promise Combinators Concurrency Patterns Parallel Requests
Definisi
Promise combinators adalah helper bawaan JavaScript untuk menggabungkan beberapa Promise dan menentukan apa yang harus dilakukan ketika hasilnya selesai, gagal, atau sebagian berhasil. Yang paling sering dipakai adalah Promise.all(), Promise.allSettled(), Promise.race(), dan Promise.any().
Di luar combinators, ada juga pola concurrency yang sering muncul saat kerja harian:
- sequencing: request dijalankan satu per satu
- parallel requests: banyak request dijalankan bersamaan
- bounded concurrency: request paralel dibatasi jumlahnya
Topik ini sering kelihatan sederhana, tapi di praktiknya justru jadi sumber bug yang paling sering: loading state salah, error handling kepotong, request kebanyakan, atau hasil pertama yang datang bukan hasil yang benar-benar kita butuhkan.
Kapan topik ini dipakai
Pakai combinators dan concurrency patterns saat kamu berurusan dengan banyak Promise sekaligus, misalnya:
- ambil data dari beberapa endpoint sebelum render halaman
- jalankan beberapa request independen agar UI lebih cepat
- tunggu semua proses selesai sebelum lanjut ke langkah berikutnya
- ambil hasil tercepat dari beberapa sumber cadangan
- kumpulkan hasil sukses sebanyak mungkin walau ada sebagian yang gagal
- validasi urutan operasi yang saling bergantung
Kalau hanya ada satu request, combinator biasanya tidak perlu. Begitu ada dua atau lebih Promise dengan relasi hasil yang berbeda, pilihan combinator mulai penting.
Peta cepat pilihan
| Kebutuhan | Pilihan |
|---|---|
| Semua Promise harus sukses, kalau satu gagal langsung gagal | Promise.all() |
| Ingin tahu semua hasil, sukses dan gagal, tanpa short-circuit | Promise.allSettled() |
| Ingin hasil pertama yang selesai, apa pun statusnya | Promise.race() |
| Ingin hasil sukses pertama, abaikan yang gagal sampai semuanya gagal | Promise.any() |
| Operasi harus berurutan | sequencing dengan await satu per satu |
| Operasi independen dan ingin cepat | parallel requests dengan Promise.all() |
Cara kerja dasar Promise combinators
Semua combinator menerima iterable berisi Promise atau nilai yang bisa diubah menjadi Promise. Hasil akhirnya tetap sebuah Promise baru.
Hal pentingnya:
- combinator tidak menjalankan request; ia hanya menggabungkan Promise yang sudah ada
- kalau sumber Promise belum dibuat, ya tidak ada yang bisa digabungkan
- urutan input tetap dipertahankan di hasil, kecuali
race()danany()yang hanya peduli hasil pertama
Promise.all()
Promise.all() dipakai saat semua hasil dibutuhkan. Kalau satu Promise gagal, seluruh hasil langsung ditolak.
Perilaku
- resolve kalau semua Promise sukses
- reject segera saat salah satu Promise reject
- hasil resolve berupa array dengan urutan sesuai input, bukan urutan selesai
Kapan dipakai
Pakai Promise.all() saat semua dependency memang wajib ada.
Contoh:
- load profile, settings, dan permissions sekaligus sebelum halaman dirender
- ambil beberapa resource yang saling melengkapi
- menunggu seluruh tugas selesai sebelum melanjutkan proses berikutnya
Contoh
const [user, posts, notifications] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/notifications').then(r => r.json()),
]);Kalau salah satu request gagal, await di atas melempar error dan tiga hasil itu tidak akan tersedia sebagai array sukses.
Jebakan umum
- mengira
Promise.all()mengembalikan hasil sesuai urutan selesai; yang benar sesuai urutan input - lupa satu request gagal bisa membatalkan seluruh kumpulan
- memakai
Promise.all()untuk data yang sebenarnya boleh sebagian sukses - tidak menangani error di level yang tepat, lalu semua error terlihat sama
Promise.allSettled()
Promise.allSettled() dipakai saat kamu ingin tahu semua hasil, apa pun statusnya.
Perilaku
- selalu resolve setelah semua Promise selesai
- tidak reject hanya karena ada Promise gagal
- hasil berupa array objek dengan bentuk:
{ status: 'fulfilled', value: ... }
{ status: 'rejected', reason: ... }Kapan dipakai
Pakai ini saat kegagalan parsial masih bisa diterima.
Contoh:
- dashboard yang menampilkan beberapa widget independen
- sinkronisasi batch yang boleh sebagian gagal
- proses audit atau logging yang butuh semua outcome
- load data dari beberapa sumber, lalu tampilkan mana yang berhasil
Contoh
const results = await Promise.allSettled([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/notifications').then(r => r.json()),
]);
const success = results
.filter(item => item.status === 'fulfilled')
.map(item => item.value);
const failed = results
.filter(item => item.status === 'rejected')
.map(item => item.reason);Jebakan umum
- mengira
allSettled()otomatis memberi nilai sukses langsung; tetap perlu cekstatus - lupa mengolah
reasonuntuk item gagal - memakai
allSettled()padahal satu kegagalan harus menghentikan alur
Promise.race()
Promise.race() selesai begitu Promise pertama settle: bisa resolved atau rejected.
Perilaku
- hasil mengikuti Promise paling cepat settle
- yang pertama selesai bisa sukses atau gagal
- tidak menunggu Promise lain selesai
Kapan dipakai
Pakai race() saat yang penting adalah hasil paling cepat.
Contoh:
- timeout manual dengan Promise pembanding
- memilih respons tercepat dari beberapa sumber jika perilaku itu memang diinginkan
- fallback yang sangat spesifik berdasarkan race condition yang terkontrol
Contoh timeout
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Request timeout')), ms);
});
}
const response = await Promise.race([
fetch('/api/search'),
timeout(3000),
]);Jebakan umum
- mengira
race()akan menunggu hasil sukses pertama; tidak, reject pertama juga menang - tidak membatalkan Promise lain setelah race selesai
- memakai
race()untuk timeout tanpa abort request yang sebenarnya masih berjalan
Kalau kamu benar-benar ingin menghentikan request fetch yang kalah, gunakan AbortController, bukan race() saja.
Promise.any()
Promise.any() resolve saat Promise sukses pertama muncul. Ia mengabaikan reject sampai semua Promise gagal.
Perilaku
- resolve ketika ada satu Promise fulfilled pertama
- reject hanya jika semua Promise gagal
- kalau semua gagal, error-nya adalah
AggregateError
Kapan dipakai
Pakai any() saat kamu butuh satu jawaban sukses dari beberapa kandidat.
Contoh:
- cari sumber data cadangan, ambil yang hidup lebih dulu
- coba beberapa endpoint mirror
- load konten dari beberapa lokasi, asal salah satu berhasil
Contoh
const data = await Promise.any([
fetch('https://api-a.example.com/data').then(r => r.json()),
fetch('https://api-b.example.com/data').then(r => r.json()),
fetch('https://api-c.example.com/data').then(r => r.json()),
]);Jebakan umum
- mengira
any()sama denganrace(); bedanya besar:any()mengabaikan reject awal - lupa menangani
AggregateErrorsaat semua kandidat gagal - tidak sadar request yang kalah tetap berjalan kalau tidak dibatalkan
Sequencing: jalan satu per satu
Sequencing dipakai saat urutan eksekusi penting, atau request berikutnya bergantung pada hasil request sebelumnya.
Kapan dipakai
- request B butuh id dari request A
- operasi harus diproses dalam urutan tertentu
- server punya rate limit ketat dan request paralel justru bikin error
- efek samping harus terjadi berurutan, misalnya create → update → publish
Contoh
const user = await fetch('/api/user').then(r => r.json());
const profile = await fetch(`/api/profile/${user.id}`).then(r => r.json());
const settings = await fetch(`/api/settings/${user.id}`).then(r => r.json());Di sini request kedua dan ketiga tidak bisa dijalankan sebelum user.id tersedia.
Jebakan umum
- menulis
awaitberurutan untuk request yang sebenarnya independen - tidak sengaja membuat alur lambat padahal bisa paralel
- mengira sequencing lebih aman untuk semua kasus; kadang cuma lebih lambat
Parallel requests: jalan bersamaan
Parallel requests dipakai saat beberapa request tidak saling bergantung dan bisa dikerjakan bersamaan.
Kapan dipakai
- load beberapa endpoint independen
- gabungkan data dari beberapa sumber
- fetch detail halaman yang masing-masing tidak bergantung satu sama lain
- jalankan batch pekerjaan yang boleh selesai dalam urutan bebas
Contoh yang benar
const userPromise = fetch('/api/user').then(r => r.json());
const postsPromise = fetch('/api/posts').then(r => r.json());
const statsPromise = fetch('/api/stats').then(r => r.json());
const [user, posts, stats] = await Promise.all([
userPromise,
postsPromise,
statsPromise,
]);Kenapa ini lebih cepat
Karena request dikirim bersamaan, total waktu biasanya mendekati request paling lambat, bukan penjumlahan semuanya.
Jebakan umum
- mem-paralelkan request yang bergantung satu sama lain
- membuat terlalu banyak request sekaligus sampai kena rate limit atau saturasi koneksi
- menganggap paralel selalu lebih baik tanpa mempertimbangkan beban server dan browser
Bounded concurrency: paralel, tapi dibatasi
Kadang kita ingin tetap paralel, tapi tidak semuanya sekaligus. Ini sering dipakai saat ada banyak item, misalnya 100 URL, 1.000 record, atau job sinkronisasi besar.
Kapan dipakai
- batch besar yang kalau dieksekusi sekaligus akan terlalu berat
- API punya batas concurrent request
- ingin menjaga memori dan koneksi tetap stabil
Pola sederhana
async function processBatch(items, limit = 5) {
const queue = [...items];
const running = [];
async function runNext() {
if (queue.length === 0) return;
const item = queue.shift();
const job = handleItem(item).finally(() => {
running.splice(running.indexOf(job), 1);
});
running.push(job);
await job;
await runNext();
}
const starters = Array.from({ length: Math.min(limit, queue.length) }, () => runNext());
await Promise.all(starters);
}Pola produksi biasanya lebih rapi kalau memakai helper queue atau library concurrency limiter, tapi inti konsepnya sama: ada paralelisme, ada batasnya.
Diagram alur concurrency
Diagram outcome handling
Contoh pemilihan yang sehat
1) Render dashboard
Kalau semua widget wajib ada, pakai Promise.all().
Kalau widget independen dan lebih baik menampilkan sebagian data daripada blank total, pakai Promise.allSettled().
2) Ambil detail yang saling bergantung
Kalau ID detail baru diketahui setelah request pertama selesai, gunakan sequencing.
3) Ambil hasil dari beberapa endpoint cadangan
Kalau satu endpoint cukup dan kamu ingin yang berhasil paling cepat, gunakan Promise.any().
4) Timeout request
Kalau yang kamu kejar adalah batas waktu, Promise.race() bisa dipakai, tapi jangan lupa request asli tetap perlu dibatalkan dengan AbortController kalau memang mau berhenti total.
Jebakan umum saat mengolah banyak request
1) Salah pilih combinator
Masalah paling umum bukan syntax, tapi semantik.
all()untuk data yang boleh sebagian gagal = terlalu kerasallSettled()untuk data yang wajib lengkap = terlalu lunakrace()dipakai padahal butuh sukses pertama = salah perilakuany()dipakai padahal semua reject harus dilaporkan = informasi penting hilang
2) Lupa urutan hasil vs urutan selesai
Promise.all() mengembalikan hasil sesuai urutan input. Itu sering disalahpahami saat beberapa request selesai tidak berurutan.
3) Over-parallelization
Mengirim terlalu banyak request sekaligus bisa bikin:
- rate limit terpukul
- browser atau Node kehabisan resource sementara
- server terlihat lambat padahal masalahnya ada di client
- log dan error jadi susah dibaca karena terlalu ramai
4) Tidak membatalkan request yang kalah
race() dan any() hanya memilih hasil. Mereka tidak otomatis menghentikan Promise lain.
Kalau sumber daya penting, tambahkan cancellation dengan AbortController atau mekanisme pembatalan lain.
5) Mengabaikan error shape
Error dari allSettled() dan AggregateError dari any() punya bentuk berbeda. Kalau parser error handling terlalu sederhana, detail penting bisa hilang.
6) Menggunakan await di dalam loop tanpa sengaja
for (const id of ids) {
const item = await fetch(`/api/item/${id}`).then(r => r.json());
items.push(item);
}Kode di atas valid, tapi berjalan sequential. Kalau id independen dan jumlahnya banyak, ini sering jadi sumber lambat yang tidak disadari.
Ringkasan praktis
- pakai
Promise.all()kalau semua hasil wajib sukses - pakai
Promise.allSettled()kalau semua outcome perlu dibaca - pakai
Promise.race()kalau yang penting hasil pertama settle - pakai
Promise.any()kalau yang penting sukses pertama - pakai sequencing kalau ada dependency urutan
- pakai parallel requests kalau request independen
- batasi concurrency kalau item banyak
- jangan lupa cancellation kalau hasil yang kalah harus benar-benar berhenti
Referensi terkait
Event Loop, Promise, Microtask, dan async/awaitFetch API, Async Data Flow, JSON Parsing, dan Error Handling DasarError Handling di JavaScript: try/catch, throw, Custom Error, dan Debugging Mindset