Async & Microtask Debugging — Debugging

Async & Microtask Debugging Bug async sering paling sulit karena stack trace kehilangan konteks. Begitu code masuk ke setTimeout, fetch, atau Promise…

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"), lalu setTimeout yang mencetak "B" dengan jeda 0, lalu Promise.resolve().then yang mencetak "C", lalu queueMicrotask yang mencetak "D", lalu console.log("E"). Urutan cetaknya apa? Jawabannya ada di bagian "Urutan: sync, microtask, macrotask".

Urutan: sync, microtask, macrotask

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

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
  }
}

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

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.

Yang akan kamu pelajari