d4DetectabilityOperational debt — how invisible misuse is to your safety net
Closest to 'default linter catches the common case' (d3), +1 to d4. ESLint (cited in detection_hints.tools) can catch some misuses like await-in-loop (no-await-in-loop rule), but other common mistakes like forgetting await entirely, using async in forEach, or missing try/catch require more careful configuration or code review. Not all issues are caught by default linter rules.
e3EffortRemediation debt — work required to fix once spotted
Closest to 'simple parameterised fix' (e3). The quick_fix is replacing sequential await-in-loop with Promise.all(items.map(...)). Most fixes (adding missing await, wrapping in try/catch, replacing forEach with for...of or Promise.all) are localized pattern replacements within a single file or function, not cross-cutting refactors.
b3BurdenStructural debt — long-term weight of choosing wrong
Closest to 'localised tax' (b3). async/await is a per-function/per-module concern. While it's pervasive in modern JS codebases, the pattern itself doesn't impose heavy structural weight — it's syntactic sugar over Promises. Misuse creates localized issues rather than system-wide architectural debt. It doesn't define the system's shape the way a framework or data model choice would.
t6TrapCognitive debt — how counter-intuitive correct behaviour is
Closest to 'notable trap' (t5), +1 to t6. The misconception that await blocks the thread/event loop is a serious conceptual error that contradicts how blocking works in other languages (e.g., C#'s .Result or Python's synchronous calls). Multiple common_mistakes compound this: forEach not awaiting async callbacks is a genuine trap that contradicts intuition, forgetting await silently returns a Promise object instead of data, and sequential awaiting in loops is a performance trap. These traps go beyond a single documented gotcha but don't quite reach 'the obvious way is always wrong' (t9).
async functions always return a Promise; await pauses execution inside an async function until a Promise settles — giving asynchronous code the readability of synchronous code without blocking the event loop.
Explanation
async/await is syntactic sugar over Promises, introduced in ES2017. Marking a function async makes it return a Promise implicitly — even a bare return value is wrapped. Inside an async function, await suspends execution until the awaited Promise resolves, then resumes with the resolved value. If the Promise rejects, await throws the rejection reason, making try/catch the natural error-handling pattern. Crucially, await does not block the thread — while waiting, the event loop is free to run other tasks. Multiple independent async operations should be parallelised with Promise.all() rather than awaited sequentially. Top-level await is supported in ES modules. Common pitfalls: forgetting await (getting a Promise instead of its value), sequential awaiting in a loop instead of parallelising, and unhandled rejections when an async function throws inside an event handler with no wrapping try/catch.
Watch Out
⚠ An async function called inside addEventListener or forEach without await propagates an unhandled Promise rejection that is very difficult to debug — always add try/catch inside async event handlers.
Common Misconception
✗ await does not block the thread or pause the event loop — it only suspends the current async function, allowing other callbacks and tasks to run while waiting.
Why It Matters
async/await eliminates callback hell and Promise chain nesting, making asynchronous code readable and maintainable — most modern JS codebases including Node.js backends and browser fetch calls rely on it.
Common Mistakes
Forgetting await — const data = fetch(url) gives a Promise object, not the response body.
Awaiting in a loop sequentially when the iterations are independent — use Promise.all() to run them in parallel.
Missing try/catch around await — an unhandled rejection inside an async function crashes Node.js workers or produces silent failures in browsers.
Using async in an Array.forEach callback — forEach does not await the returned Promises, so the loop finishes before the async work completes.
Code Examples
💡 Note
The bad example awaits each fetch in sequence — 10 requests × 200ms = 2 seconds total. Promise.all fires all requests simultaneously, completing in ~200ms regardless of count.
✗ Vulnerable
// Sequential awaits — total time = sum of all request times:
async function loadAll(ids) {
const results = [];
for (const id of ids) {
results.push(await fetch(`/api/${id}`)); // waits for each in turn
}
return results;
}
✓ Fixed
// Parallel with Promise.all — total time = slowest single request:
async function loadAll(ids) {
const promises = ids.map(id => fetch(`/api/${id}`));
return Promise.all(promises); // all requests in flight simultaneously
}
🧱FUNDAMENTALS— new to this? Start with the ground floor.
Callback HelljavascriptCallback hell is when multiple nested callback functions create deeply indented, hard-to-read code that looks like a sideways pyramid.
Recognizing callback hell helps you understand why Promises and async/await were invented, and teaches you to spot code structure problems before they become maintenance nightmares.
💡 If your code indents more than three levels deep with callbacks, refactor to Promises or async/await.
JavaScriptjavascriptThe programming language of the browser — it reads and modifies the page (the DOM), reacts to user events, and fetches data without reloading.
JavaScript is the only language browsers execute, so every interactive behaviour on the web goes through it. Its two defining traits — single-threaded event loop and loose typing (== coercion) — explain the majority of both its bugs and its design patterns.
💡 Default to const, use === always, and reach for let only when a value genuinely reassigns.