JavaScript Async
JavaScript is single-threaded but non-blocking. The engine offloads I/O to the browser/Node runtime and resumes via the event loop.
The Event Loop (simplified)
Key rule: Microtasks (Promises) always drain before the next Task Queue item runs. This is why Promise.resolve().then(...) runs before setTimeout(() => ..., 0).
The Three Patterns
Callbacks (avoid in new code)
fs.readFile('file.txt', (err, data) => {
if (err) throw err;
processData(data, (err, result) => { // callback hell
...
});
});
Problem: error handling is inconsistent, nesting gets out of hand fast.
Promises
fetch('/api/user')
.then(res => res.json())
.then(user => console.log(user))
.catch(err => console.error(err));
Better for chaining, but .then() chains still get verbose.
Async/Await (my default)
async function getUser(id) {
try {
const res = await fetch(`/api/user/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error('Failed to fetch user:', err);
throw err; // re-throw so caller can handle
}
}
Reads like synchronous code. Always pair with try/catch — unhandled promise rejections are silent in some environments.
Patterns I Actually Use
Parallel fetches
// Sequential (slow — waits for each)
const user = await getUser(id);
const posts = await getPosts(id);
// Parallel (fast — runs simultaneously)
const [user, posts] = await Promise.all([getUser(id), getPosts(id)]);
Abort on unmount (React)
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => { if (err.name !== 'AbortError') console.error(err); });
return () => controller.abort();
}, []);
Race with timeout
const withTimeout = (promise, ms) =>
Promise.race([
promise,
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms))
]);
Gotchas
await inside forEach doesn't work:
// Broken — forEach ignores returned promise
items.forEach(async (item) => { await process(item); });
// Fix
for (const item of items) { await process(item); }
// Or parallel: await Promise.all(items.map(process));
async functions always return a Promise:
async function getNum() { return 42; }
getNum(); // → Promise<42>, not 42