As a JavaScript developer, you're likely familiar with the event loop handling asynchronous operations. However, microtasks provide a crucial middle layer between synchronous code execution and the macrotask queue that can significantly impact your application's behavior.
Think of microtasks as high-priority tasks that JavaScript executes immediately after the current synchronous operation, but before the next macrotask (like setTimeout
callbacks) or render updates.
- Promise resolution callbacks (
.then()
,.catch()
,.finally()
) - The
queueMicrotask()
API async/await
operations
Let's examine a contrived example that shows how microtasks queue and execute. This code intentionally creates a chain of function calls using both synchronous code and microtasks to illustrate execution order:
Click to view the JavaScript…
// microtasks.js
'use strict'
async function a() {
console.log('a invoked...')
b()
console.log('a: waiting on microtasks...')
await microtasks()
stack.push({a: `${(performance.now() - start).toFixed(2)}ms`})
console.log('a: done', stack)
}
/**
* This style exhibits the same behavior as the async function
* Use-case may dictate which style to use
function a() {
console.log('a invoked...')
b()
console.log('a: waiting on microtasks...')
microtasks().then(() => {
stack.push({a: `${(performance.now() - start).toFixed(2)}ms`})
console.log('a: done', stack)
})
}
*/
function b() {
console.log('b: invoked...')
queueMicrotask(() => c())
stack.push({b: `${(performance.now() - start).toFixed(2)}ms`})
console.log('b: done', stack)
}
function c() {
console.log('c: invoked...')
queueMicrotask(() => d())
stack.push({c: `${(performance.now() - start).toFixed(2)}ms`})
console.log('c: done', stack)
}
function d() {
console.log('d: invoked...')
stack.push({d: `${(performance.now() - start).toFixed(2)}ms`})
console.log('d: done', stack)
}
async function microtasks() {
await Promise.resolve()
}
const start = performance.now()
const stack = []
a()
Take a moment to predict:
- Will function
c
execute beforea
finishes? - What order will the timestamps appear in?
- How many console.log statements will run before microtasks begin?
Click to view the Output…
node microtasks.js
a invoked...
b: invoked...
b: done [ { b: '3.91ms' } ]
a: waiting on microtasks...
c: invoked...
c: done [ { b: '3.91ms' }, { c: '4.17ms' } ]
d: invoked...
d: done [ { b: '3.91ms' }, { c: '4.17ms' }, { d: '4.34ms' } ]
a: done [ { b: '3.91ms' }, { c: '4.17ms' }, { d: '4.34ms' }, { a: '4.69ms' } ]
The sequence diagrams help visualize exactly how JavaScript processes these operations:
Click to view the Sequence Diagram…
┌────────┐ ┌────────┐ ┌────────┐ ┌────────────┐ ┌────────┐ ┌────────┐
│ main │ │ a() │ │ b() │ │ microtask │ │ c() │ │ d() │
│ │ │ │ │ │ │ queue │ │ │ │ │
└───┬────┘ └───┬────┘ └───┬────┘ └─────┬──────┘ └───┬────┘ └───┬────┘
│ │ │ │ │ │
01⟩─────────▶︎│ │ │ │ │
│ call a │ │ │ │ │
│ 02⟩─────────▶︎│ │ │ │
│ │ call b │ │ │ │
│ │ 03⟩───────────▶︎│ │ │
│ │ │ queue c │ │ │
│ │ │ │ │ │
│ │◀︎─────────⟨04 │ │ │
│ │ return │ │ │ │
│ │ │ │ │ │
│ 05⟩────────────────────────▶︎│ │ │
│ │ await microtasks │ │ │
│ │ │ │ │ │
│ │ │ 06⟩───────────▶︎│ │
│ │ │ │ call c │ │
│ │ │ │ │ │
│ │ │ │◀︎───────────⟨07 │
│ │ │ │ queue d │ │
│ │ │ │ │ │
│ │ │ │◀︎───────────⟨08 │
│ │ │ │ return │ │
│ │ │ │ │ │
│ │ │ 09⟩────────────────────────▶︎│
│ │ │ │ call d │ │
│ │ │ │ │ │
│ │ │ │◀︎────────────────────────⟨10
│ │ │ │ │ return │
│ │◀︎────────────────────────⟨11 │ │
│ │ microtasks done │ │ │
│ │ │ │ │ │
│◀︎─────────⟨12 │ │ │ │
│ return │ │ │ │ │
│ │ │ │ │ │
Click to view the Execution Tree…
01. Main Calls a()
│
├─ logs "a invoked..."
│
02. └─ calls b()
│ ├─ logs "b: invoked..."
│ │
03. ├─ queues c() as microtask
│ ├─ adds timestamp {b: 3.91ms}
│ └─ logs "b: done"
│
04. returns to a()
│
05. └─ logs "a: waiting..."
└─ awaits microtasks
Microtask Queue Processing:
│
06. └─ c() executes
│ ├─ logs "c: invoked..."
│ │
07. ├─ queues d() as microtask
│ ├─ adds timestamp {c: 4.17ms}
08. └─ logs "c: done"
│
09. └─ d() executes
│ ├─ logs "d: invoked..."
│ ├─ adds timestamp {d: 4.34ms}
10. └─ logs "d: done"
│
11. └─ microtasks complete
│ a() resumes
│ ├─ adds timestamp {a: 4.69ms}
│ └─ logs "a: done"
│
12. returns to main
Understanding microtask behavior is crucial for:
- Predictable Async Code: Microtasks execute in a guaranteed order before the next macrotask or render
- Performance Optimization: Microtasks run within the same tick of the event loop
- Debugging: When troubleshooting async code, remember microtasks take priority over macrotasks
- Framework Development: Many modern frameworks rely on microtask timing for state updates
- Microtasks can delay macrotasks indefinitely if they keep spawning new microtasks
- Promise chains create microtasks for each
.then()
callback async/await
syntax implicitly creates microtasks
This knowledge is especially valuable when building complex applications or working with modern JavaScript frameworks that heavily utilize the Promise API and async operations.