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.
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:
- Macrotask queue —
setTimeout,setInterval, I/O, UI render - Microtask queue — Promise callback (
.then),queueMicrotask,MutationObserver
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
- Buka Sources panel → panel kanan Call Stack
- Centang checkbox Async
- Sekarang saat paused di callback async, call stack menampilkan "Async" separator — frame sebelum
setTimeout/fetchterlihat lagi - 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
- Pasti kah
.catchatau try/catch terpasang? - Semua panggilan async sudah di-
await? - Logika tergantung urutan? Cek microtask vs macrotask.
- Ada shared state yang bisa race?
- DevTools: aktifkan "Async" di Call Stack.
- Node: naikkan
stack-trace-limitkalau stack ter-truncate.