Bug async adalah bug yang muncul karena kode gak jalan di waktu yang kamu kira: hasilnya belum ada, error-nya gak ditangkap siapa pun, atau urutannya beda dari urutan baris yang kamu tulis. JavaScript cuma punya satu jalur kerja (thread), mirip satu kasir di warung. Kasir menyelesaikan pembeli yang sedang dilayani sampai beres. Urusan "nanti dulu" masuk ke dua antrean: antrean kilat untuk urusan kecil yang harus segera beres, dan antrean biasa. Tiap kali satu pembeli selesai, kasir menghabiskan antrean kilat dulu, baru memanggil satu orang dari antrean biasa.
🧪 Tebakan cepat: kodenya
console.log("A"), lalusetTimeoutyang mencetak"B"dengan jeda 0, laluPromise.resolve().thenyang mencetak"C", laluqueueMicrotaskyang mencetak"D", laluconsole.log("E"). Urutan cetaknya apa? Jawabannya ada di bagian "Urutan: sync, microtask, macrotask".
Urutan: sync, microtask, macrotask
- Kode sync: kode yang langsung jalan baris demi baris sampai habis. Ini pembeli yang sedang dilayani.
- Microtask (antrean kilat): callback
.thendan.catchdari Promise yang sudah selesai, sisa functionasyncsetelahawait, danqueueMicrotask(fn). - Macrotask (antrean biasa, sering disebut task saja): callback
setTimeoutdansetInterval, event klik, dan jawaban dari jaringan. - Event loop: aturan kasirnya. Jalankan kode sync sampai habis, kosongkan semua microtask, baru ambil satu macrotask. Setelah macrotask itu selesai, kosongkan microtask lagi, dan seterusnya.
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
queueMicrotask(() => console.log("D"));
console.log("E");
| Langkah | Yang jalan | Dicetak | Antrean microtask | Antrean macrotask |
|---|---|---|---|---|
| 1 | console.log("A") |
A | kosong | kosong |
| 2 | setTimeout(...) |
kosong | B | |
| 3 | Promise.resolve().then(...) |
C | B | |
| 4 | queueMicrotask(...) |
C, D | B | |
| 5 | console.log("E") |
E | C, D | B |
| 6 | kode sync habis, kosongkan microtask | C, D | kosong | B |
| 7 | ambil satu macrotask | B | kosong | kosong |
Hasilnya A, E, C, D, B. Itu jawaban tebakannya. setTimeout(fn, 0) bukan "sekarang juga", tapi "setelah kode sync dan semua microtask beres".
Bug 1: lupa await
await artinya "tunggu Promise ini selesai, baru lanjut ke baris berikutnya". Tanpa await, function jalan terus, dan Promise-nya dibiarkan jalan sendiri tanpa ada yang menunggu. Promise seperti ini disebut floating promise (Promise yang melayang). Ini database palsu di Lab halaman ini: dia menjawab 800 ms kemudian, dan saat saklar "Database gagal" nyala, jawabannya error.
const db = {
insert(order) {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("DB down")), 800); // gagal setelah 800 ms
});
},
};
async function saveOrder(order) {
db.insert(order); // lupa await
return { status: "saved" };
}
async function saveOrderFixed(order) {
await db.insert(order); // tunggu sampai insert selesai
return { status: "saved" };
}
| Waktu | saveOrder (lupa await) |
saveOrderFixed |
|---|---|---|
| 0 ms | langsung return { status: "saved" }, UI bilang "Tersimpan" |
berhenti di await, menunggu |
| 800 ms | insert gagal, gak ada yang menunggu: Uncaught (in promise) Error: DB down |
await melempar Error: DB down ke pemanggilnya |
db.insert(order)tanpaawaitmengembalikan Promise yang langsung dibuang. Function-nya return duluan, sebelum datanya tersimpan.await db.insert(order)menahan function di baris itu. Kalau Promise-nya gagal,awaitmelempar error-nya di baris itu juga, persis sepertithrow.- Rule ESLint
@typescript-eslint/no-floating-promisesmenandai Promise yang gak di-await, di-return, atau di-.catch.
Bug 2: unhandled rejection
Promise yang gagal disebut rejected. Kalau gak ada .catch() atau try/catch yang menangkapnya, namanya unhandled rejection. Browser mencetak Uncaught (in promise) Error: DB down di Console, dan user gak dapat pesan apa-apa. Di Node.js 15 ke atas, unhandled rejection bahkan menghentikan prosesnya.
async function handleSave(order) {
try {
await saveOrderFixed(order);
tampilkan("Tersimpan");
} catch (err) {
tampilkan("Gagal menyimpan: " + err.message); // Gagal menyimpan: DB down
}
}
try/catchcuma menangkap error dari Promise yang di-awaitdi dalamtry. DengansaveOrderversi lupaawait,try/catchini gak menolong: error-nya muncul 800 ms kemudian, saattrysudah lama selesai.- Di rantai
.then, pasang.catch(fn)di ujungnya:saveOrderFixed(order).then(...).catch((err) => ...). window.addEventListener("unhandledrejection", (e) => ...)adalah jaring pengaman terakhir untuk mencatat yang lolos. Dia bukan penggantitry/catch.
Mengecek ketiganya di latihan
Latihan memodelkan ketiga bug ini dengan data biasa. Event loop jadi tiga kali filter:
function simulateEventLoop(tasks) {
const sync = tasks.filter((t) => t.type === "sync").map((t) => t.label);
const micro = tasks.filter((t) => t.type === "micro").map((t) => t.label);
const macro = tasks.filter((t) => t.type === "macro").map((t) => t.label);
return [...sync, ...micro, ...macro];
}
function detectUnhandledRejection(promises) {
return promises.filter((p) => !p.chained).map((p) => p.id);
}
findForgottenAwait membaca kode sebagai teks, baris demi baris, dengan aturan kasar: baris yang memanggil function dan diakhiri ;, tapi gak mengandung kata await, return, atau function, dicurigai lupa await.
| Baris | Isinya | Dilaporkan? | Alasannya |
|---|---|---|---|
| 1 | async function save(order) { |
gak | ada kata function |
| 2 | db.insert(order); |
ya | memanggil function tanpa await |
| 3 | const data = await fetchUser(); |
gak | ada await |
| 4 | return { status: 'ok' }; |
gak | ada return |
| 5 | } |
gak | gak memanggil apa-apa |
| 6 | saveOrder(o); |
ya | memanggil function tanpa await |
Jadi tiga baris output latihannya: A,E,C,D,B, lalu 2,6, lalu p2,p3 (p2 dan p3 yang chained-nya false). Aturan teks ini cuma untuk latihan: dia juga akan mencurigai console.log("x");. Tool sungguhan seperti ESLint tadi memakai type, jadi tahu mana yang benar-benar Promise.
💡 Async stack trace di DevTools
Error di dalam function async tetap membawa jejak siapa yang menunggunya. Di err.stack kamu akan ketemu baris seperti at async handleSave (app.js:4:5). Chrome DevTools juga menyambung Call Stack melewati await dan setTimeout secara otomatis, jadi saat berhenti di breakpoint di dalam callback, kamu tetap bisa melihat function yang menjadwalkannya. Pasang console.trace("label") untuk mencetak jejak itu tanpa berhenti.
⚠️ Jebakan yang sering ketemu
- Mengira
setTimeout(fn, 0)jalan duluan. Dia selalu menunggu kode sync dan semua microtask selesai. try/catchtanpaawait. Error dari Promise yang gak di-awaitlolos daritry, lalu jadiUncaught (in promise).- Mencetak hasil function
asynctanpaawait.console.log(saveOrder(o))mencetakPromise { <pending> }, bukan{ status: "saved" }. - Menulis urutannya dengan tangan di latihan.
simulateEventLoopjuga diuji dengan task lain (M1,S1,T1,S2harus jadiS1, S2, M1, T1), jadi urutannya harus dihitung daritype.
TL;DR: Event loop menjalankan kode sync sampai habis, lalu semua microtask (.then, await, queueMicrotask), baru satu macrotask (setTimeout, event), jadi A, setTimeout B, then C, queueMicrotask D, E tercetak A, E, C, D, B. Lupa await bikin function return sebelum pekerjaannya selesai, dan Promise yang melayang itu kalau gagal jadi unhandled rejection: Uncaught (in promise) Error: .... Tangkap dengan try/catch di sekitar await, atau .catch() di ujung rantai, dan ingat try/catch gak menolong Promise yang gak di-await.