Async/Await Deep Dive: Beyond the Basics
Master the subtle behaviors of async/await, including microtask ordering, error propagation, and parallelism.
Await Suspends, Then Resumes via Microtask
Every await schedules the resumption as a microtask, even when the awaited value is already resolved:
``js
async function example() {
console.log('1');
await Promise.resolve();
console.log('3'); // runs after current synchronous code
}
example();
console.log('2'); // logs before '3'
`
This means await Promise.resolve(value) is not free — you give up the rest of the current tick.
Sequential vs Parallel
Naive code awaits in series, multiplying latency:
`js
const a = await fetchA();
const b = await fetchB(); // waits for A even though independent
`
Parallelize with Promise.all:
`js
const [a, b] = await Promise.all([fetchA(), fetchB()]);
`
For 5 independent 200ms requests, this is the difference between 1000ms and 200ms.
Error Handling Without Drowning in try/catch
Wrap once at the boundary, not on every line. For utilities, return a tuple:
`js
async function safe(promise) {
try { return [null, await promise]; }
catch (err) { return [err, null]; }
}
const [err, data] = await safe(fetchUser(id));
`
Avoid Async in forEach
Array.prototype.forEach ignores returned promises. Use for...of for sequential iteration or Promise.all(array.map(asyncFn))` for parallel.
Pair this with the [JavaScript event loop explained](/blog/javascript-event-loop-explained) post to see exactly when your callbacks run.