Back to Blog
JavaScriptqueueMicrotaskAsyncEvent Loop

JavaScript queueMicrotask API Guide

Master JavaScript queueMicrotask for scheduling microtasks with precise timing control.

B
Bootspring Team
Engineering
June 12, 2019
6 min read

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 - microtask

Event 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// setTimeout

Batching 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 updates

State 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 2

Custom 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// setTimeout

Best 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.

Share this article

Help spread the word about Bootspring