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…

Async & Microtask Debugging

Bug async sering paling sulit karena stack trace kehilangan konteks. Begitu code masuk ke setTimeout, fetch, atau Promise, tumpukan call sinkron lenyap — yang tersisa hanya "callback terpanggil dari suatu tempat di event loop".

Kenapa Async Stack Susah

function handleClick() {
  setTimeout(() => {
    doWork(); // ← kalau error di sini, stack tidak menunjukkan handleClick
  }, 0);
}

Stack trace default:

Error at doWork
    at setTimeout callback (app.js:3)

Tidak ada jejak handleClick — padahal itulah yang memicu. Browser dan Node modern menyimpan "async stack" tapi tidak selalu di-surface secara default.

<style>.lbl{font:12px sans-serif;fill:#0f172a}.sub{font:11px sans-serif;fill:#475569}.warn{font:11px sans-serif;fill:#b91c1c}</style> Sync stack (sebelum setTimeout) handleClick() setTimeout(cb, 0) stack context hilang Event loop tick berikutnya (anonymous) doWork() → throw stack hanya 1 frame — siapa yang trigger? tidak tahu. Solusi: Chrome DevTools → Sources → centang "Async" di Call Stack DevTools menyatukan async stack → terlihat handleClick di bawah callback

Promise Chain vs async/await — Mana Stack Trace Lebih Baik?

Promise chain (.then):

loadUser().then(u => processUser(u)).then(p => saveProfile(p));
// Error di saveProfile → stack: "at saveProfile" (tidak jelas chain-nya)

async/await:

async function main() {
  const u = await loadUser();
  const p = await processUser(u);
  await saveProfile(p);
}
// Error → stack: main() di atas saveProfile, karena await menyimpan frame

async/await memberi stack trace lebih baik karena engine (V8, SpiderMonkey) menjaga async frame antar await. Ini salah satu alasan async/await lebih disarankan daripada .then chain panjang untuk debugging.

Microtask vs Macrotask Order

Event loop JS punya dua antrian:

Aturan: setelah tiap macrotask, drain SEMUA microtask sebelum macrotask berikutnya.

console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
queueMicrotask(() => console.log("D"));
console.log("E");

// Output: A, E, C, D, B
// Sync (A, E) → microtask (C, D) → macrotask (B)

Bug umum: kode mengasumsikan setTimeout(fn, 0) jalan "segera" padahal semua Promise pending sudah drain dulu.

Chrome DevTools: Async Stack Trace

  1. Buka Sources panel → panel kanan Call Stack
  2. Centang checkbox Async
  3. Sekarang saat paused di callback async, call stack menampilkan "Async" separator — frame sebelum setTimeout/fetch terlihat lagi
  4. Debugger bisa pause di baris sync yang trigger async work

Node.js: --stack-trace-limit

Default Node hanya tampilkan 10 frame terakhir. Untuk async bug yang dalam:

node --stack-trace-limit=50 app.js
# atau di kode:
Error.stackTraceLimit = 50;

Juga berguna: node --async-stack-traces (on by default di Node 12+) — menyambung stack antar await.

Bug Async Umum

1. Unhandled Rejection

async function loadData() {
  const res = await fetch("/api/data"); // reject kalau network error
  return res.json();
}
loadData(); // ← tidak ada .catch → unhandled rejection!

Node akan crash (di Node 15+), browser tampilkan warning. Fix: selalu .catch() atau bungkus di try/await.

2. Forgotten await

async function saveOrder(order) {
  db.insert(order); // ← lupa await!
  return { status: "saved" }; // balas sebelum insert selesai
}

TypeScript/ESLint: aktifkan rule @typescript-eslint/no-floating-promises.

3. Race Condition

let cache = null;
async function getData() {
  if (cache) return cache;
  cache = await fetch("/api/data").then(r => r.json());
  return cache;
}
// Kalau dipanggil 2x bersamaan → fetch 2x karena cache masih null di call kedua
// Fix: cache Promise, bukan hasil
let cachePromise = null;
async function getData() {
  cachePromise ??= fetch("/api/data").then(r => r.json());
  return cachePromise;
}

queueMicrotask Visualization

queueMicrotask(fn) menjadwalkan fn di akhir microtask queue — sebelum macrotask berikutnya:

queueMicrotask(() => console.log("after sync, before timer"));
setTimeout(() => console.log("timer"), 0);
console.log("sync");
// sync → after sync, before timer → timer

Berguna untuk defer work tanpa setTimeout overhead (lebih cepat, prioritas tinggi).

console.trace() di Async Code

async function deep() {
  console.trace("masuk deep");
  // print stack trace ke console — termasuk async frame kalau --async-stack-traces aktif
}

Pasang console.trace di suspect point untuk tahu siapa pemanggil tanpa perlu pause.

Checklist Debug Async

  1. Pasti kah .catch atau try/catch terpasang?
  2. Semua panggilan async sudah di-await?
  3. Logika tergantung urutan? Cek microtask vs macrotask.
  4. Ada shared state yang bisa race?
  5. DevTools: aktifkan "Async" di Call Stack.
  6. Node: naikkan stack-trace-limit kalau stack ter-truncate.

Yang akan kamu pelajari