The queueMicrotask function schedules a microtask to run before the next task in the event loop. Here's how to use it effectively.
Basic Usage#
1console.log('1');
2
3queueMicrotask(() => {
4 console.log('2 - microtask');
5});
6
7console.log('3');
8
9// Output:
10// 1
11// 3
12// 2 - microtaskEvent Loop Order#
1console.log('1 - sync');
2
3setTimeout(() => {
4 console.log('2 - setTimeout (macrotask)');
5}, 0);
6
7Promise.resolve().then(() => {
8 console.log('3 - Promise.then (microtask)');
9});
10
11queueMicrotask(() => {
12 console.log('4 - queueMicrotask');
13});
14
15console.log('5 - sync');
16
17// Output:
18// 1 - sync
19// 5 - sync
20// 3 - Promise.then (microtask)
21// 4 - queueMicrotask
22// 5 - setTimeout (macrotask)
23
24// Microtasks run before macrotasks!vs setTimeout#
1// setTimeout schedules a macrotask
2function withSetTimeout(callback) {
3 setTimeout(callback, 0);
4}
5
6// queueMicrotask schedules a microtask
7function withMicrotask(callback) {
8 queueMicrotask(callback);
9}
10
11// Microtask runs first
12withSetTimeout(() => console.log('setTimeout'));
13withMicrotask(() => console.log('microtask'));
14
15// Output:
16// microtask
17// setTimeoutBatching Updates#
1class BatchedUpdater {
2 constructor() {
3 this.pending = false;
4 this.updates = [];
5 }
6
7 scheduleUpdate(update) {
8 this.updates.push(update);
9
10 if (!this.pending) {
11 this.pending = true;
12 queueMicrotask(() => this.flush());
13 }
14 }
15
16 flush() {
17 const updates = this.updates;
18 this.updates = [];
19 this.pending = false;
20
21 // Process all batched updates
22 updates.forEach((update) => update());
23 console.log(`Processed ${updates.length} updates`);
24 }
25}
26
27const updater = new BatchedUpdater();
28
29updater.scheduleUpdate(() => console.log('Update 1'));
30updater.scheduleUpdate(() => console.log('Update 2'));
31updater.scheduleUpdate(() => console.log('Update 3'));
32
33console.log('Scheduling complete');
34
35// Output:
36// Scheduling complete
37// Update 1
38// Update 2
39// Update 3
40// Processed 3 updatesState Synchronization#
1class ReactiveState {
2 constructor(initialValue) {
3 this._value = initialValue;
4 this._listeners = new Set();
5 this._notifyScheduled = false;
6 }
7
8 get value() {
9 return this._value;
10 }
11
12 set value(newValue) {
13 if (this._value !== newValue) {
14 this._value = newValue;
15 this._scheduleNotify();
16 }
17 }
18
19 _scheduleNotify() {
20 if (!this._notifyScheduled) {
21 this._notifyScheduled = true;
22 queueMicrotask(() => {
23 this._notifyScheduled = false;
24 this._listeners.forEach((listener) => listener(this._value));
25 });
26 }
27 }
28
29 subscribe(listener) {
30 this._listeners.add(listener);
31 return () => this._listeners.delete(listener);
32 }
33}
34
35// Usage
36const state = new ReactiveState(0);
37
38state.subscribe((value) => console.log('Value:', value));
39
40state.value = 1;
41state.value = 2;
42state.value = 3;
43
44console.log('Changes queued');
45
46// Output:
47// Changes queued
48// Value: 3 (only one notification with final value)Deferred Callbacks#
1class DeferredCallback {
2 constructor() {
3 this.callbacks = [];
4 this.scheduled = false;
5 }
6
7 defer(callback) {
8 this.callbacks.push(callback);
9
10 if (!this.scheduled) {
11 this.scheduled = true;
12 queueMicrotask(() => {
13 this.scheduled = false;
14 const toRun = this.callbacks;
15 this.callbacks = [];
16 toRun.forEach((cb) => cb());
17 });
18 }
19 }
20}
21
22const deferred = new DeferredCallback();
23
24function doWork() {
25 console.log('Starting work');
26
27 deferred.defer(() => console.log('Cleanup 1'));
28 deferred.defer(() => console.log('Cleanup 2'));
29
30 console.log('Work done');
31}
32
33doWork();
34
35// Output:
36// Starting work
37// Work done
38// Cleanup 1
39// Cleanup 2Custom Promise-like#
1class MicroPromise {
2 constructor(executor) {
3 this.state = 'pending';
4 this.value = undefined;
5 this.handlers = [];
6
7 try {
8 executor(
9 (value) => this._resolve(value),
10 (error) => this._reject(error)
11 );
12 } catch (error) {
13 this._reject(error);
14 }
15 }
16
17 _resolve(value) {
18 if (this.state !== 'pending') return;
19 this.state = 'fulfilled';
20 this.value = value;
21 this._executeHandlers();
22 }
23
24 _reject(error) {
25 if (this.state !== 'pending') return;
26 this.state = 'rejected';
27 this.value = error;
28 this._executeHandlers();
29 }
30
31 _executeHandlers() {
32 queueMicrotask(() => {
33 this.handlers.forEach((handler) => {
34 if (this.state === 'fulfilled' && handler.onFulfilled) {
35 handler.onFulfilled(this.value);
36 } else if (this.state === 'rejected' && handler.onRejected) {
37 handler.onRejected(this.value);
38 }
39 });
40 });
41 }
42
43 then(onFulfilled, onRejected) {
44 return new MicroPromise((resolve, reject) => {
45 this.handlers.push({
46 onFulfilled: (value) => {
47 try {
48 const result = onFulfilled ? onFulfilled(value) : value;
49 resolve(result);
50 } catch (error) {
51 reject(error);
52 }
53 },
54 onRejected: (error) => {
55 try {
56 if (onRejected) {
57 const result = onRejected(error);
58 resolve(result);
59 } else {
60 reject(error);
61 }
62 } catch (e) {
63 reject(e);
64 }
65 },
66 });
67
68 if (this.state !== 'pending') {
69 this._executeHandlers();
70 }
71 });
72 }
73}Event Coalescing#
1class EventCoalescer {
2 constructor() {
3 this.pendingEvents = new Map();
4 }
5
6 emit(eventName, data) {
7 // Store latest data for this event
8 this.pendingEvents.set(eventName, data);
9
10 // Schedule if not already scheduled
11 if (this.pendingEvents.size === 1) {
12 queueMicrotask(() => this.flush());
13 }
14 }
15
16 flush() {
17 const events = new Map(this.pendingEvents);
18 this.pendingEvents.clear();
19
20 for (const [eventName, data] of events) {
21 console.log(`Emitting ${eventName}:`, data);
22 // Dispatch actual event here
23 }
24 }
25}
26
27const coalescer = new EventCoalescer();
28
29coalescer.emit('resize', { width: 100 });
30coalescer.emit('resize', { width: 200 });
31coalescer.emit('resize', { width: 300 });
32coalescer.emit('scroll', { top: 50 });
33
34// Only emits once per event type with latest value:
35// Emitting resize: { width: 300 }
36// Emitting scroll: { top: 50 }DOM Update Batching#
1class DOMBatcher {
2 constructor() {
3 this.reads = [];
4 this.writes = [];
5 this.scheduled = false;
6 }
7
8 read(callback) {
9 this.reads.push(callback);
10 this.schedule();
11 }
12
13 write(callback) {
14 this.writes.push(callback);
15 this.schedule();
16 }
17
18 schedule() {
19 if (!this.scheduled) {
20 this.scheduled = true;
21 queueMicrotask(() => this.flush());
22 }
23 }
24
25 flush() {
26 this.scheduled = false;
27
28 // Execute all reads first (avoid layout thrashing)
29 const reads = this.reads;
30 this.reads = [];
31 reads.forEach((read) => read());
32
33 // Then execute all writes
34 const writes = this.writes;
35 this.writes = [];
36 writes.forEach((write) => write());
37 }
38}
39
40const batcher = new DOMBatcher();
41
42// Batched DOM operations
43function updateElements() {
44 const elements = document.querySelectorAll('.item');
45
46 elements.forEach((el) => {
47 // Read operations
48 batcher.read(() => {
49 const height = el.offsetHeight;
50 // Store height for later
51 });
52
53 // Write operations
54 batcher.write(() => {
55 el.style.transform = 'scale(1.1)';
56 });
57 });
58}Error Handling#
1// Errors in microtasks are reported to the global error handler
2queueMicrotask(() => {
3 throw new Error('Microtask error');
4});
5
6// Can be caught with window.onerror or process.on('uncaughtException')
7
8// For controlled error handling, use try-catch inside
9queueMicrotask(() => {
10 try {
11 // risky operation
12 throw new Error('Handled error');
13 } catch (error) {
14 console.error('Caught:', error.message);
15 }
16});
17
18// Or wrap in a helper
19function safeMicrotask(callback) {
20 queueMicrotask(() => {
21 try {
22 callback();
23 } catch (error) {
24 console.error('Microtask error:', error);
25 }
26 });
27}Comparison#
1// Different scheduling methods
2function compare() {
3 // Macrotask - lowest priority
4 setTimeout(() => console.log('setTimeout'), 0);
5
6 // Macrotask - similar to setTimeout
7 setImmediate?.(() => console.log('setImmediate'));
8
9 // Microtask - high priority
10 Promise.resolve().then(() => console.log('Promise.then'));
11
12 // Microtask - high priority (same as Promise.then)
13 queueMicrotask(() => console.log('queueMicrotask'));
14
15 // Sync - highest priority
16 console.log('sync');
17}
18
19// Output order:
20// sync
21// Promise.then
22// queueMicrotask
23// setImmediate (Node.js)
24// setTimeoutBest Practices#
Use Cases:
✓ Batching multiple updates
✓ Deferring cleanup tasks
✓ Coalescing events
✓ Custom async primitives
Timing:
✓ Runs before next macrotask
✓ Same queue as Promise.then
✓ After current sync code
✓ Before requestAnimationFrame
Patterns:
✓ Schedule once, process many
✓ Store latest value
✓ Clear pending on flush
✓ Handle errors properly
Avoid:
✗ Long-running microtasks
✗ Infinite microtask loops
✗ Blocking the event loop
✗ Using when setTimeout suffices
Conclusion#
queueMicrotask schedules functions to run in the microtask queue, after the current synchronous code but before the next macrotask (like setTimeout). Use it for batching updates, coalescing events, and creating custom async primitives. Microtasks run with higher priority than macrotasks, making them ideal for tasks that should complete before the browser renders or handles I/O. Be careful not to create infinite loops or block the event loop with long-running microtasks.