Mengapa JavaScript yang Single-Threaded Tidak Membeku Saat Mengambil Data Jaringan?
Mesin utama JavaScript menjalankan kode Anda pada **satu utas tunggal (Single Thread / Satu Call Stack)**: pada satu detik yang sama, ia hanya bisa mengeksekusi satu baris perintah JavaScript.
Lalu bagaimana peramban tetap bisa menggulir halaman dengan mulus dan merespons klik tombol sementara program sedang menunggu balasan API dari server yang memakan waktu 2 detik?
Jawabannya adalah kolaborasi antara Call Stack, Host/Web APIs milik peramban (atau libuv di Node.js), Microtask Queue, Task (Macrotask) Queue, dan sang pengatur lalu lintas yang disebut Event Loop:
+------------------------+ +---------------------------------------+
| CALL STACK | | WEB APIs / RUNTIME HOST |
| (Menjalankan fungsi JS | ---> | (Timer setTimeout, Jaringan fetch(), |
| satu per satu) | | Pendengar DOM addEventListener) |
+------------------------+ +---------------------------------------+
^ │
│ (Setelah selesai:)
[ EVENT LOOP ] ▼
│ +---------------------------------------+
├────── Prioritas 1 │ MICROTASK QUEUE |
│ (Habiskan | (Callback Promise .then/catch/finally,|
│ semua!) | kelanjutan baris setelah await, |
│ | queueMicrotask) |
│ +---------------------------------------+
│ +---------------------------------------+
└────── Prioritas 2 │ TASK / MACROTASK QUEUE |
(Ambil satu | (Callback setTimeout, setInterval, |
per satu) | Event klik/papan ketik DOM) |
+---------------------------------------+
Aturan Emas Urutan Eksekusi Event Loop:
- Jalankan semua kode sinkron yang ada di dalam Call Stack sampai Call Stack benar-benar kosong.
- Begitu Call Stack kosong, Event Loop memeriksa Microtask Queue terlebih dahulu dan menjalankan seluruh microtask sampai antrean microtask benar-benar habis (termasuk microtask baru yang ditambahkan oleh microtask tadi!).
- Setelah Microtask Queue kosong, barulah Event Loop mengambil satu tugas dari Task (Macrotask) Queue (misalnya callback
setTimeout), memasukkannya ke Call Stack, lalu kembali lagi ke langkah 2!
Mari kita buktikan dengan menebak urutan keluaran kode berikut:
console.log("1. Sinkron Awal");
setTimeout(() => {
console.log("4. Macrotask (setTimeout 0ms)");
}, 0);
Promise.resolve().then(() => {
console.log("3. Microtask (Promise.then)");
});
console.log("2. Sinkron Akhir");
// Urutan yang tercetak di layar SELALU:
// 1. Sinkron Awal
// 2. Sinkron Akhir
// 3. Microtask (Promise.then) <-- Dijalankan LEBIH DULU daripada setTimeout 0ms!
// 4. Macrotask (setTimeout 0ms)
2. Memahami Promise dan Tiga Status Siklus Hidupnya
Sebuah Promise adalah objek yang mewakili hasil akhir dari operasi asinkron yang sedang berlangsung. Setiap Promise selalu berada dalam salah satu dari tiga kondisi (states):
pending(Menunggu): Operasi masih berjalan.fulfilled(Berhasil): Operasi tuntas dengan sebuah nilai hasil (value).rejected(Gagal): Operasi gagal dengan sebuah alasan galat (reason/ objekError).
Ketika sebuah Promise sudah berubah menjadi fulfilled atau rejected, ia disebut settled (Tuntas) dan statusnya terkunci selamanya (tidak dapat berubah lagi).
// Membungkus API berbasis callback lama (seperti setTimeout) menjadi Promise
function tunggu(milidetik) {
return new Promise((resolve, reject) => {
if (milidetik < 0) {
reject(new RangeError("Durasi waktu tunggu tidak boleh negatif."));
return;
}
setTimeout(() => {
resolve(`Selesai menunggu selama ${milidetik} ms`);
}, milidetik);
});
}
Promise.withResolvers()Jika Anda perlu mengekstrak fungsi resolve dan reject agar bisa dipanggil dari luar lingkup callback new Promise(...) (misalnya pada komponen pembatalan atau antrean event), kini Anda dapat menggunakan const { promise, resolve, reject } = Promise.withResolvers(); (status Baseline Widely Available; didukung semua peramban utama sejak Maret 2024)!
3. Menulis Kode Asinkron yang Bersih dengan async dan await
Sintaks async / await dibangun di atas Promise untuk membuat kode asinkron tampak dan mengalir selayaknya kode sinkron biasa:
- Menambahkan kata kunci
asyncdi depan fungsi menjamin fungsi tersebut selalu mengembalikan sebuahPromise. - Kata kunci
awaitmenjeda eksekusi fungsiasynctersebut (tanpa memblokir utas utama / Call Stack!) hinggaPromisedi sebelah kanannya selesai (settled). JikaPromisetersebut rejected,awaitakan melempar galatnya sehingga dapat ditangkap dengan bloktry...catchstandar!
async function muatRingkasanPengguna(idPengguna) {
try {
const pesan = await tunggu(200);
return { idPengguna, status: pesan };
} catch (galat) {
console.error("Gagal memuat ringkasan:", galat.message);
throw galat;
}
}
4. Konkurensi Promise: Jangan Menjalankan await Berurutan Jika Bisa Paralel!
Perhatikan jebakan performa paling sering saat menggunakan async/await:
// LAMBAT (Berurutan / Sequential): Total waktu = 300ms + 300ms + 300ms = 900ms!
// Karena mengambil daftar kelas sama sekali tidak bergantung pada hasil profil,
// menunggu satu per satu membuang waktu pengguna secara percuma.
async function muatDasborLambat() {
const profil = await ambilData("/api/profil"); // 300ms
const kelas = await ambilData("/api/kelas"); // 300ms
const notifikasi = await ambilData("/api/notifikasi"); // 300ms
return { profil, kelas, notifikasi };
}
// CEPAT (Paralel / Concurrent dengan Promise.all): Total waktu hanya ~300ms!
async function muatDasborCepat() {
const [profil, kelas, notifikasi] = await Promise.all([
ambilData("/api/profil"),
ambilData("/api/kelas"),
ambilData("/api/notifikasi")
]);
return { profil, kelas, notifikasi };
}
Memilih Metode Kombinator Promise yang Tepat
| Metode | Kapan Selesai? | Apa yang Terjadi Jika Ada Satu saja yang Gagal (Rejected)? | Kasus Penggunaan Terbaik |
|---|---|---|---|
Promise.all([...]) | Saat semua berhasil. | **Langsung gagal seketika (fail-fast)** dan membuang hasil lainnya. | Mengambil beberapa data yang semuanya wajib ada agar halaman bisa tampil. |
Promise.allSettled([...]) | Saat semua tuntas (baik berhasil maupun gagal). | Tidak pernah gagal! Mengembalikan array objek { status: "fulfilled", value } atau { status: "rejected", reason }. | Mengambil widget dasbor independen (jika widget cuaca gagal, widget berita tetap tampil!). |
Promise.race([...]) | Saat Promise pertama tuntas (berhasil ataupun gagal). | Mengikuti hasil Promise tercepat pertama. | Membatasi waktu tunggu (timeout) sebuah operasi. |
Promise.any([...]) | Saat Promise **pertama berhasil (fulfilled)**. | Mengabaikan yang gagal; hanya melempar AggregateError jika semuanya gagal. | Mengambil aset dari beberapa server cermin (mirror CDN) tercepat. |
Latihan Praktik: Mengambil Data Multi-Layanan secara Tangguh dengan Promise.allSettled
Buatlah berkas asinkron-paralel.js dan jalankan dengan Node.js:
- Buat fungsi
simulasiRequest(namaLayanan, waktuMs, harusGagal = false)yang mengembalikanPromise. - Jalankan 3 permintaan sekaligus menggunakan
await Promise.allSettled([...]): Layanan Profil (100ms, sukses), Layanan Rekomendasi (150ms, gagal), dan Layanan Progres (80ms, sukses). - Pisahkan hasil yang
status === "fulfilled"danstatus === "rejected"agar aplikasi tetap menampilkan data profil dan progres sambil mencatat peringatan untuk layanan rekomendasi!