Skip to content
JavaScript Basics: Understand Microtask Ordering and Batch Small Updates

JavaScript Basics: Understand Microtask Ordering and Batch Small Updates

October 22, 2026

queueMicrotask() schedules a callback after the current synchronous work finishes, before the event loop moves on to a later task. Promise reactions also use the microtask queue. This makes microtasks useful for consistent callback timing and combining a small burst of updates, but not for moving expensive work off the main thread.

These three independent examples were tested on Linux with Node.js v24.19.0. They use only built-in features, make no network requests, and include assertions. The exact files require Node.js 15.0.0 or later because they import node:assert/strict; the submodule was introduced in Node.js 15. That historical minimum is not a recommendation to use an obsolete release: use a currently supported Node.js version. Only the installed version above was executed for this tutorial.

Prepare a scratch directory

You need Bash, a text editor, Node.js on your PATH, and the mktemp, rm, and rmdir utilities. Keep the same terminal open. Save each named source file as UTF-8 in this directory; .mjs selects an ES module. The guarded commands stop if creating or entering the scratch directory fails.

node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

The tested runtime prints:

v24.19.0

1. Observe synchronous work, microtasks, and a timer

Save this as observe-microtask-order.mjs:

import assert from 'node:assert/strict';

const steps = ['start'];
Promise.resolve().then(() => steps.push('promise'));
queueMicrotask(() => {
  steps.push('queued');
  queueMicrotask(() => steps.push('nested'));
});
steps.push('end');
assert.deepEqual(steps, ['start', 'end']);

setTimeout(() => {
  steps.push('timer');
  assert.deepEqual(steps, ['start', 'end', 'promise', 'queued', 'nested', 'timer']);
  console.log(steps.join(' -> '));
}, 0);

Run it:

node observe-microtask-order.mjs

Output:

start -> end -> promise -> queued -> nested -> timer

Scheduling a callback does not interrupt the current code: end is recorded before either queued callback runs. The fulfilled promise reaction was queued first. The nested microtask joins the queue while it is being drained and still runs before this timer callback.

The zero-delay timer is a later opportunity to run, not an exact zero-millisecond deadline. This example asserts sequence, never elapsed time. It also deliberately excludes process.nextTick(), whose relationship to module execution can complicate beginner examples. Node documents the shared promise/microtask queue and the CommonJS versus ES module distinction in its microtask guidance.

2. Keep cache-hit callbacks asynchronous

Save this as normalize-cache-callbacks.mjs:

import assert from 'node:assert/strict';

const cache = new Map([['guide', 'Cached guide']]);
const source = new Map([['faq', 'Fresh FAQ']]);
function lookup(key, callback) {
  if (cache.has(key)) {
    const value = cache.get(key);
    queueMicrotask(() => callback(value));
    return;
  }
  Promise.resolve(source.get(key)).then(callback);
}
const events = [];
lookup('guide', value => events.push(value));
lookup('faq', value => events.push(value));
events.push('requests returned');
assert.deepEqual(events, ['requests returned']);

setTimeout(() => {
  assert.deepEqual(events, ['requests returned', 'Cached guide', 'Fresh FAQ']);
  console.log(events.join(' | '));
}, 0);

Run it:

node normalize-cache-callbacks.mjs

Output:

requests returned | Cached guide | Fresh FAQ

Both paths deliver a value after lookup() returns. Without the queued cache hit, a caller could observe different timing depending on whether an entry happened to be cached. Map.has() also distinguishes a missing entry from a present entry whose value is undefined. The example’s source is an in-memory stand-in, so no network or filesystem work occurs.

This callback exercise does not unify error handling: an exception in a microtask callback is an uncaught exception, while one inside .then() rejects the resulting promise. Production code often benefits from a promise-returning API with an explicit rejection path. See Node’s queueMicrotask documentation.

3. Combine a synchronous burst into one batch

Save this as batch-invalidated-sections.mjs:

import assert from 'node:assert/strict';

const pending = new Set();
const batches = [];
let scheduled = false;
function invalidate(section) {
  pending.add(section);
  if (scheduled) return;
  scheduled = true;
  queueMicrotask(() => {
    const batch = [...pending];
    pending.clear();
    scheduled = false;
    batches.push(batch);
  });
}
invalidate('header');
invalidate('footer');
invalidate('header');
assert.equal(batches.length, 0);
queueMicrotask(() => invalidate('sidebar'));

setTimeout(() => {
  assert.deepEqual(batches, [['header', 'footer'], ['sidebar']]);
  console.log(batches.map(batch => batch.join(', ')).join(' | '));
}, 0);

Run it:

node batch-invalidated-sections.mjs

Output:

header, footer | sidebar

The first call schedules a flush, and further synchronous calls reuse it. A Set removes the duplicate header while preserving insertion order. By the time the separately queued sidebar callback runs, the first flush has cleared its state, so a second batch is scheduled. The batching boundary is the flush, not an entire event-loop turn.

This is useful for inexpensive notifications or invalidation bookkeeping. Keep each flush bounded: repeatedly scheduling more microtasks can prevent timers and I/O from making progress. Microtasks do not create parallel execution and should not be used as an endless work queue. If actual processing is expensive, use an appropriate task scheduler or worker instead.

Clean up

In the same terminal, remove only the named files, return to the original directory, and remove the now-empty scratch directory:

rm -- observe-microtask-order.mjs normalize-cache-callbacks.mjs batch-invalidated-sections.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on