8

Consider the following polyfill for queueMicrotask.

if (typeof window.queueMicrotask !== "function") {
  window.queueMicrotask = function (callback) {
    Promise.resolve()
      .then(callback)
      .catch(e => setTimeout(() => { throw e; }));
  };
}

The description on MDN states.

It creates a microtask by using a promise that resolves immediately, falling back to using a timeout if the promise can't be created.

The queue-microtask library also uses the same polyfill. Here's what its documentation says.

  • Optimal performance in all modern environments.
    • Use queueMicrotask in modern environments (optimal)
    • Fallback to Promise.resolve().then(fn) in Node.js 10 and earlier, and old browsers (optimal)
    • Fallback to setTimeout in JS environments without Promise (slow)

This raises more questions than answers.

  • Wouldn't Promise be undefined in JS environments without promises?
  • Why are we throwing the error instead of calling callback within setTimeout?
  • Why are we using a separate catch instead of passing the error handler to then?
  • How does this polyfill fallback to using setTimeout when “the promise can't be created”?
  • When would the promise not be created?

I would have expected the polyfill to be implemented as follows.

if (typeof window.queueMicrotask !== "function") {
  window.queueMicrotask = callback =>
    typeof Promise === "function" && typeof Promise.resolve === "function"
      ? Promise.resolve().then(callback)
      : setTimeout(callback, 0);
}

What's the reason why it's not implemented so?

Edit: I was going through the commit history of the queue-microtask library and I found this commit.

@@ -1,9 +1,8 @@
-let resolvedPromise
+let promise

 module.exports = typeof queueMicrotask === 'function'
   ? queueMicrotask
-  : (typeof Promise === 'function' ? (resolvedPromise = Promise.resolve()) : false)
-    ? cb => resolvedPromise
-      .then(cb)
-      .catch(err => setTimeout(() => { throw err }, 0))
-    : cb => setTimeout(cb, 0)
+  // reuse resolved promise, and allocate it lazily
+  : cb => (promise || (promise = Promise.resolve()))
+    .then(cb)
+    .catch(err => setTimeout(() => { throw err }, 0))

So, it seems as though this library did indeed fallback to using cb => setTimeout(cb, 0). However, this was later removed. It might have been a mistake which went unnoticed. As for the MDN article, they might have just copied the snippet blindly from this library.

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299

1 Answers1

9

You are entirely right in your main points, this polyfill won't work if there is no Promise in the environment, and I did edit the MDN article to now call it a "monkey-patch" as it is what it is and I removed the reference to "fallback" as there isn't.

To answer your questions:

  • Yes Promise would be undefined and thus the polyfill would just throw:

delete window.queueMicrotask;
delete window.Promise;

if (typeof window.queueMicrotask !== "function") {
  window.queueMicrotask = function (callback) {
    Promise.resolve()
      .then(callback)
      .catch(e => setTimeout(() => { throw e; }));
  };
}

queueMicrotask( () => console.log('hello') );
But this "shim" is apparently only aimed at "modern engines".
  • The MDN editor that did introduce that exception throwing here did so because the specs ask that queueMicroTask reports any exception that would be thrown during callback execution. The Promise chain would "swallow" this exception (it wouldn't get thrown globally), so to get out of this Promise chain, we have to call setTimeout from inside the .catch() handler.

  • Handling from the second parameter of then wouldn't handle Exceptions thrown from the callback execution, which is just what we want to do here.

  • It doesn't fallback to anything else than Promise, as we shown in the previous bullets it would just throw in case Promise is not defined, and the setTimeout is only used to throw the Exception out of the Promise chain.

  • The Promise would not be created by Promise.resolve() when that function would be something else than a correct Promise implementation. And if it's the case, there is like no chance it returns a catchable object either ;) But As you may have caught now, only the explanation text was completely mislead+ing.


Now, a note about your monkey-patch which still might be improvable a little bit:

  • This editor was actually correct that the error should be reported, the catch + setTimeout should be there.

  • queueMicrotask should throw if callback is not Callable.

  • Nitpick, but the callback passed to .then() will be called with one argument undefined, queueMicrotask calls its callback without any argument.

  • Nitpick again, checking every time if Promise is available doesn't sound great, either Promise is defined from the beginning, either you'll use a polyfill you don't know how they managed the asynchronicity.

  • More importantly (?) you may want to add support for more environments.


The queue a microtask algorithm was already part of the Web standards before Promises make their way to browsers: MutationObserver queues microtasks too, and it was supported in IE11 (unlike Promises).

function queueMutationObserverMicrotask( callback ) {
  var observer = new MutationObserver( function() {
    callback();
    observer.disconnect();
  } );
  var target = document.createElement( 'div' );
  observer.observe( target, { attributes: true } );
  target.setAttribute( 'data-foo', '' );
}

Promise.resolve().then( () => console.log( 'Promise 1' ) );
queueMutationObserverMicrotask( () => console.log('from mutation') );
Promise.resolve().then( () => console.log( 'Promise 2' ) );

In node.js < 0.11, process.nextTick() was the closest to what microtasks are, so you may want to add it too (it's short enough).

if( typeof process === "object" && typeof process.nextTick === "function" ) {
  process.nextTick( callback );
}

So all in all, our improved polyfill would look like

(function() {
'use strict';

// lazy get globalThis, there might be better ways
const globalObj = typeof globalThis === "object" ? globalThis :
  typeof global === "object" ? global :
  typeof window === "object" ? window :
  typeof self === 'object' ? self :
  Function('return this')();

if (typeof queueMicrotask !== "function") {

  const checkIsCallable = (callback) => {
    if( typeof callback !== 'function' ) {
      throw new TypeError( "Failed to execute 'queueMicrotask': the callback provided as parameter 1 is not a function" );
    }  
  };

  if( typeof Promise === "function" && typeof Promise.resolve === "function" ) {
    globalObj.queueMicrotask = (callback) => {
      checkIsCallable( callback );
      Promise.resolve()
        .then( () => callback() ) // call with no arguments
        // if any error occurs during callback execution,
        // throw it back to globalObj (using setTimeout to get out of Promise chain)
        .catch( (err) => setTimeout( () => { throw err; } ) );
   };
  }
  else if( typeof MutationObserver === 'function' ) {
    globalObj.queueMicrotask = (callback) => {
      checkIsCallable( callback );
      const observer = new MutationObserver( function() {
        callback();
        observer.disconnect();
      } );
      const target = document.createElement( 'div' );
      observer.observe( target, { attributes: true } );
      target.setAttribute( 'data-foo', '');
    };
  }
  else if( typeof process === "object" && typeof process.nextTick === "function" ) {
    globalObj.queueMicrotask = (callback) => {
      checkIsCallable( callback );
      process.nextTick( callback );
    };
  }
  else {
    globalObj.queueMicrotask = (callback) => {
      checkIsCallable( callback );
      setTimeout( callback, 0 );
    }
  }
}
})();

queueMicrotask( () => console.log( 'microtask' ) );
console.log( 'sync' );
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • This is one of the best explained answers, I'd point, we can even fallback to `setTimeout` in case someone is fine with queuing it to the Macrotask queue if the Microtask queue is unavailable in the current env. – Sunny R Gupta Jul 26 '20 at 14:08
  • @Kaiido your knowledge about JS and browser amazes me every time i read your answers on SO ;) – Dachstein Aug 22 '20 at 11:55