Browser Microtasks and the Render Queue
A browser microtask is a small piece of JavaScript work that the browser runs after the current JavaScript stack is empty, before it takes another task or usually renders a new frame. The microtask queue is the ordered queue holding that work.
Promise callbacks—functions passed to .then(), .catch(), or .finally()—are the most familiar things placed in this queue. The queue lets JavaScript finish a logical chain of follow-up work before the browser moves on to unrelated work such as a click handler, a timer, or a paint.
The problem microtasks solve
Browsers must balance two kinds of work:
- Run JavaScript, such as event handlers and network callbacks.
- Keep the page responsive by processing input and updating the display.
JavaScript runs on a single main thread in the usual browser model. While one piece of JavaScript is running, the browser cannot run another piece of JavaScript or update the page in the middle of it.
Suppose a promise callback ran as a completely separate task. The browser could render or process an input event between the promise settling and its callback. A chain of related callbacks could then be interrupted by unrelated work, and observers could see intermediate states that the chain was meant to update together.
Microtasks provide a middle ground: they do not interrupt the current JavaScript execution, but they run sooner than the next ordinary task. This gives promise-based code a predictable point to continue while still giving the browser a boundary at which it can later render and handle other work.
Tasks and microtasks
A task is a top-level unit of work selected by the browser’s event loop. Examples include running a timer callback, dispatching a user event such as click, or invoking a callback for some completed browser operation. People often call these “macrotasks,” but the platform generally calls them tasks.
A simplified turn of the browser event loop looks like this:
- Select one task.
- Run its JavaScript until the call stack is empty.
- Drain the microtask queue.
- If appropriate, update the rendered page.
- Start another task later.
The browser has more scheduling rules than this model suggests, and it does not necessarily render after every task. But the ordering is the important part: a microtask checkpoint happens before the browser proceeds to a rendering opportunity or starts the next task.
How a promise callback enters the queue
Calling .then() does not immediately call its callback. Instead, it registers a promise reaction. When the promise becomes fulfilled or rejected, the browser schedules that reaction as a microtask.
console.log("start");
Promise.resolve().then(() => {
console.log("promise callback");
});
console.log("end");
The output is:
start
end
promise callback
The current task is still running when Promise.resolve() and .then() execute. The promise callback is therefore queued for later in the same event-loop turn. The browser first finishes the surrounding script, including console.log("end"), then drains microtasks.
A promise that settles later works the same way at the point of settlement:
fetch("/profile")
.then(response => response.json())
.then(profile => {
renderProfile(profile);
});
When the fetch operation produces a response, the browser runs the relevant task that handles that operation. Settling the promise schedules its reaction as a microtask. If response.json() returns another promise, the next .then() callback is scheduled only after that promise settles.
This is why a .then() callback is asynchronous even for an already fulfilled promise. It is queued; it does not run inline inside the call to .then().
async and await use the same machinery. When an async function reaches an await, its remaining code resumes through promise reactions, so that continuation runs as a microtask rather than immediately on the same stack.
Draining the queue
“Drain” means more than running the microtasks that were present at the beginning. The browser repeatedly takes the oldest microtask and runs it. If that microtask queues another microtask, the new one is also run before the checkpoint ends.
queueMicrotask(() => {
console.log("first");
queueMicrotask(() => console.log("third"));
});
queueMicrotask(() => console.log("second"));
The output is:
first
second
third
queueMicrotask() explicitly adds a function to the microtask queue. Promise reactions are added implicitly when promises settle. A MutationObserver callback, which reports certain DOM changes, is another browser feature that uses microtask scheduling.
The queue is drained before rendering because the browser wants the next visible frame to reflect a settled JavaScript turn, not an arbitrary midpoint in a promise chain. For example, code can update several related DOM properties through promise callbacks, and the browser normally paints only after those callbacks have completed. It also drains microtasks before the next task so a promise continuation associated with the current task is not overtaken by a later click or timer task.
This ordering is useful, but it is not a promise that every microtask runs immediately or that every task produces a frame. Rendering is optional at each opportunity: the browser may decide that no frame is needed, or that it has other scheduling constraints.
The responsiveness cost
Microtasks have priority over the next task and the next rendering opportunity. A long microtask, or an endless chain of microtasks, can therefore prevent the browser from painting and handling input.
function keepRunning() {
queueMicrotask(keepRunning);
}
keepRunning();
This never gives the event loop a chance to reach another task or a rendering opportunity. Real incidents are usually less obvious: a promise chain processes a large data set, and each step queues more microtasks. The page appears frozen even though no single callback looks catastrophic.
For work that can be divided across turns, code may need to yield back to the task queue—for example by using a timer or another appropriate browser scheduling mechanism—so the browser can handle input and render. The practical rule is to treat microtasks as short, same-turn follow-up work, not as a general-purpose background queue.
When a stack trace says a callback ran from a promise reaction, or when a UI update seems to wait until an entire chain of .then() callbacks completes, the microtask queue explains the behavior: the current task finished, the browser drained all promise continuations, and only then could it move toward the next task or frame.