Kenapa ini penting
Kamu sudah memakai setTimeout dan Promise. Tetapi pernahkah kamu bertanya: jika JavaScript hanya bisa mengerjakan satu hal dalam satu waktu, bagaimana ia bisa "menunggu" timer sambil tetap menjalankan kode lain? Dan kenapa Promise.resolve().then(...) selalu jalan sebelum setTimeout(..., 0) padahal keduanya "secepatnya"? Jawabannya adalah event loop, mesin penjadwal di jantung JavaScript.
Memahami event loop membuat kamu bisa memprediksi urutan output kode asinkron, mengerti kenapa halaman membeku saat ada loop berat, dan menulis async/await dengan benar. async/await adalah cara modern menulis Promise yang terlihat seperti kode biasa dari atas ke bawah. Ini yang akan kamu pakai sehari-hari, jadi kamu perlu tahu persis kapan kode "berhenti menunggu" dan kapan tidak, termasuk cara menangani error-nya.
Konsep inti
Bagian-bagian mesin
Analogi: sebuah restoran dengan satu koki (thread JavaScript).
| Komponen | Analogi | Peran |
|---|---|---|
| Call stack | Meja kerja koki | Function yang sedang dikerjakan, tumpuk-menumpuk (terakhir masuk, pertama keluar) |
| Web API / Node API | Oven, kompor, kurir | Timer, fetch, file, event klik dikerjakan di luar thread utama |
| Task queue (macrotask) | Antrean pesanan biasa | Callback setTimeout, setInterval, event, I/O |
| Microtask queue | Antrean prioritas VIP | Callback .then/.catch/.finally, lanjutan setelah await, queueMicrotask |
| Event loop | Manajer restoran | Mengatur apa yang dikerjakan koki berikutnya |
Aturan event loop
- Jalankan semua kode sinkron sampai call stack kosong.
- Kosongkan seluruh microtask queue (termasuk microtask baru yang muncul selama proses ini).
- Ambil satu task dari task queue, jalankan.
- Kembali ke langkah 2. (Di browser, render layar bisa terjadi di antara task.)
Konsekuensinya:
- Microtask selalu didahulukan dari task.
Promise.thenjalan sebelumsetTimeout(..., 0). - Selama call stack sibuk (misalnya loop berat 3 detik), tidak ada callback, klik, atau render yang bisa diproses. Halaman membeku.
- Microtask yang terus membuat microtask baru bisa "membuat lapar" task queue, sehingga timer tidak pernah jalan.
async dan await
- Function yang diberi
asyncselalu mengembalikan Promise.return nilaimenjadi resolve,throwmenjadi reject. await promisemenjeda function async itu saja (bukan seluruh program) sampai Promise settled, lalu mengembalikan value-nya. Jika rejected,awaitmelempar error yang bisa ditangkaptry/catch.- Sisa function setelah
awaitdijadwalkan sebagai microtask. Kode di luar function tetap berjalan. awaitboleh dipakai di level teratas (top-level await) di ES module (file.mjsatau"type": "module", serta<script type="module">).
// Dua cara yang setara
function ambil() { return getData().then((d) => olah(d)); }
async function ambil() { const d = await getData(); return olah(d); }
Error handling async
| Gaya | Cara menangkap |
|---|---|
| Promise chain | .catch(fn) |
| async/await | try { await ... } catch (e) { ... } |
| Memanggil function async dari luar | fungsiAsync().catch(...) atau await di dalam try |
Penolakan yang tidak ditangani memicu event unhandledrejection (browser) atau menghentikan proses Node.js. Pastikan setiap rantai berakhir di tempat yang menangani error.
Berurutan vs paralel
await di dalam loop menjalankan pekerjaan satu per satu. Jika pekerjaannya tidak saling bergantung, mulai semuanya dulu lalu tunggu bersama (Promise.all, dibahas lengkap di modul berikutnya). Ini bisa memangkas waktu tunggu drastis.
Contoh kode
// 1. Tebak urutan output: sinkron → microtask → task
console.log("1. sinkron awal");
setTimeout(() => console.log("6. task (setTimeout 0)"), 0);
Promise.resolve()
.then(() => console.log("3. microtask then #1"))
.then(() => console.log("5. microtask then #2"));
queueMicrotask(() => console.log("4. microtask queueMicrotask"));
(async () => {
console.log("2. di dalam async (sebelum await, masih sinkron)");
await null;
console.log("4b. setelah await (microtask)");
})();
console.log("2b. sinkron akhir");
// → 1. sinkron awal
// → 2. di dalam async (sebelum await, masih sinkron)
// → 2b. sinkron akhir
// → 3. microtask then #1
// → 4. microtask queueMicrotask
// → 4b. setelah await (microtask)
// → 5. microtask then #2
// → 6. task (setTimeout 0)
Microtask dijalankan sesuai urutan masuk antrean: then #1, queueMicrotask, dan lanjutan await sudah mengantre saat kode sinkron berjalan. then #2 baru masuk antrean setelah then #1 selesai, jadi ia paling akhir di antara microtask.
// 2. async/await dengan simulasi API
const tunggu = (ms) => new Promise((r) => setTimeout(r, ms));
async function ambilKurs(mataUang) {
await tunggu(100); // simulasi jaringan
const KURS = { USD: 16250, SGD: 12480, JPY: 107 };
if (!(mataUang in KURS)) throw new Error(`Kurs ${mataUang} tidak tersedia`);
return KURS[mataUang];
}
async function konversi(jumlah, mataUang) {
try {
const kurs = await ambilKurs(mataUang); // berhenti di sini, tapi tidak memblokir program
return `${jumlah} ${mataUang} = Rp${(jumlah * kurs).toLocaleString("id-ID")}`;
} catch (err) {
return `Gagal konversi: ${err.message}`;
} finally {
console.log(`(selesai memproses ${mataUang})`);
}
}
// 3. Function async mengembalikan Promise
const hasil = konversi(50, "USD");
console.log(hasil instanceof Promise); // → true
// 4. Berurutan vs paralel
async function main() {
console.log(await hasil);
// → (selesai memproses USD)
// → 50 USD = Rp812.500
console.log(await konversi(1000, "EUR"));
// → (selesai memproses EUR)
// → Gagal konversi: Kurs EUR tidak tersedia
console.time("berurutan");
const usd = await ambilKurs("USD");
const sgd = await ambilKurs("SGD");
const jpy = await ambilKurs("JPY");
console.timeEnd("berurutan"); // → berurutan: 303ms (±3 x 100 ms)
console.time("paralel");
const [u, s, j] = await Promise.all([ambilKurs("USD"), ambilKurs("SGD"), ambilKurs("JPY")]);
console.timeEnd("paralel"); // → paralel: 101ms (±100 ms)
console.log(usd === u && sgd === s && jpy === j); // → true
// 5. Jebakan: forEach tidak menunggu async
const daftar = ["USD", "SGD"];
daftar.forEach(async (m) => console.log("forEach:", m, await ambilKurs(m)));
console.log("Ini tercetak SEBELUM hasil forEach!");
await tunggu(150);
// → Ini tercetak SEBELUM hasil forEach!
// → forEach: USD 16250
// → forEach: SGD 12480
// Yang benar untuk berurutan: for...of
for (const m of daftar) {
console.log("for...of:", m, await ambilKurs(m));
}
// → for...of: USD 16250
// → for...of: SGD 12480
}
main().catch((err) => console.error("Error tak terduga:", err)); // tangkap di level teratas
// 6. Loop berat memblokir semuanya (timer pun harus menunggu)
setTimeout(() => {
const mulai = Date.now();
setTimeout(() => console.log(`Timer 0 ms baru jalan setelah ${Date.now() - mulai} ms`), 0);
while (Date.now() - mulai < 300) { /* blokir thread 300 ms */ }
}, 1000);
// → Timer 0 ms baru jalan setelah 300 ms
Contoh 6 adalah demonstrasi paling penting: timer 0 ms harus menunggu 300 ms karena koki (thread) sedang sibuk. Di browser, selama 300 ms itu tombol tidak bisa diklik dan animasi berhenti. Pekerjaan berat seharusnya dipecah kecil-kecil atau dipindah ke Web Worker.
Kesalahan umum
- Lupa
await.const kurs = ambilKurs("USD")menghasilkan Promise, bukan angka. Perkalian dengan Promise menghasilkanNaN. - Memakai
awaitdi dalamforEach/mapdan mengira loop menunggu.forEachtidak peduli Promise. Pakaifor...of(berurutan) atauPromise.all(arr.map(...))(paralel). awaitberurutan untuk pekerjaan yang independen. Waktunya menjadi jumlah semua tunggu. Mulai bersamaan denganPromise.all.- Tidak menangani error function async. Memanggil
main()tanpa.catchatau try/catch berujung unhandled rejection. - Mengira
awaitmenghentikan seluruh program. Ia hanya menjeda function async tempat ia berada; kode lain dan event tetap berjalan.
Latihan
- Mudah, Tanpa menjalankan, tebak urutan output:
setTimeout(() => console.log("A"));,Promise.resolve().then(() => console.log("B"));,console.log("C");. Lalu buktikan. - Sedang, Ubah rantai
login().then(u => ambilTransaksi(u.id)).then(t => hitungSaldo(t)).catch(...)menjadi functionasyncdengan try/catch. Buat function simulasinya. - Menantang, Tulis
async function prosesBertahap(items, fn, ukuranBatch)yang memprosesitemsdalam batch: tiap batch berjalan paralel (Promise.all), antar-batch berurutan. Kembalikan semua hasil sesuai urutan.
Lihat solusi
// 1
setTimeout(() => console.log("A"));
Promise.resolve().then(() => console.log("B"));
console.log("C");
// → C (sinkron)
// → B (microtask)
// → A (task)
// 2
const jeda = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));
const login = () => jeda(50, { id: 7 });
const ambilTransaksi = (id) => jeda(50, [{ jumlah: 500000 }, { jumlah: -120000 }]);
const hitungSaldo = (t) => t.reduce((s, x) => s + x.jumlah, 0);
async function tampilkanSaldo() {
try {
const user = await login();
const trx = await ambilTransaksi(user.id);
console.log("Saldo:", hitungSaldo(trx)); // → Saldo: 380000
} catch (err) {
console.log("Gagal:", err.message);
}
}
// 3
async function prosesBertahap(items, fn, ukuranBatch) {
const hasil = [];
for (let i = 0; i < items.length; i += ukuranBatch) {
const batch = items.slice(i, i + ukuranBatch);
hasil.push(...(await Promise.all(batch.map(fn))));
}
return hasil;
}
(async () => {
await jeda(10);
await tampilkanSaldo();
const kuadrat = await prosesBertahap([1, 2, 3, 4, 5], (n) => jeda(50, n * n), 2);
console.log(kuadrat); // → [ 1, 4, 9, 16, 25 ] (3 batch: ±150 ms)
})();
Ringkasan
- Event loop: jalankan kode sinkron, kosongkan semua microtask, lalu ambil satu task, ulangi.
- Microtask (
then, lanjutanawait,queueMicrotask) selalu mendahului task (setTimeout, event). - Kode sinkron yang berat memblokir timer, event, dan render.
asyncselalu mengembalikan Promise;awaitmenjeda function itu saja dan melempar jika rejected.- Tangani error dengan try/catch di dalam async dan
.catchdi level teratas; hindariawaitdiforEach.