Promises, Async/Await, and JavaScript Microtasks vs Macrotasks Explained

TL;DR:
JavaScript promises and the async/await syntax schedule their callbacks as microtasks, which always run before the next macrotask (e.g., setTimeout). Understanding this orderingâand how to leverage microtasks vs. macrotasksâlets you write predictable, non-blocking, high-performance async code.
1. Promises & async/await: The Basics
A Promise represents the eventual result of an asynchronous operation:
const promise = new Promise((resolve, reject) => { // async work if (success) resolve('Done') else reject('Error') })
.then()/.catch(): attach callbacks on fulfillment or rejection.async/await: syntactic sugar over promises for cleaner, synchronous-looking flow:
async function fetchData() { try { const data = await getJSON('/api/data') console.log(data) } catch (err) { console.error(err) } }
Under the hood, every await pauses execution, then resumes in a microtask once the promise settles.
2. Microtask Queue: Promise Callbacks & queueMicrotask
When a promise settles, its .then/.catch handler is enqueued as a microtask:
console.log('Start') Promise.resolve().then(() => console.log('Promise callback')) console.log('End') // Output: Start â End â Promise callback
- Start and End run on the call stack.
- Promise callback goes into the microtask queueâdrained immediately after the stack clears, before any timers.
You can also enqueue microtasks manually:
queueMicrotask(() => { console.log('Custom microtask') })
3. Microtasks vs. Macrotasks
| Queue Type | Enqueue By | Runs Before⌠|
|---|---|---|
| Microtask | Promise.then, queueMicrotask, MutationObserver | the next macrotask |
| Macrotask | setTimeout, setInterval, I/O callbacks, UI events | the next event loop tick after microtasks |
console.log('A') setTimeout(() => console.log('B'), 0) // macrotask Promise.resolve().then(() => console.log('C')) // microtask console.log('D') // A â D â C â B
4. How async/await Leverages Microtasks
Each await wraps the continuation in a microtask:
async function foo() { console.log('foo start') // 1 await null console.log('foo resumed') // 4 } console.log('script start') // 2 foo() console.log('script end') // 3 // Order: foo start â script start â script end â foo resumed
foo startlogs immediately.- Awaited continuation (
foo resumed) defers to a microtask, after the current stack.
5. Pitfalls & Best Practices
- Uncaught rejections: Always add
.catch()or wrapawaitin try/catch to avoid silent failures. - Starvation risk: Flooding with microtasks can delay rendering and macrotasksâuse sparingly.
- Chunk long work: Break CPU-heavy tasks into smaller macrotasks (
setTimeout(âŚ,0)) to keep the UI responsive. - Prefer async/await: Cleaner syntax and predictable microtask ordering.
Final Takeaways
- Promises and async/await schedule work as microtasks.
- Microtasks always drain before the next macrotask.
- Properly balancing microtasks vs. macrotasks leads to smooth, predictable async flows.
Master this interplay to eliminate âwhy did this run first?â surprises and write rock-solid JavaScript. Happy coding!