The JavaScript setImmedate
function has been proposed and promoted as a faster alternative to setTimeout(fn, 0)
(and cleaner than postMessage
). While the HTML spec clamps setTimeout(fn, 0)
to a minimum delay of 4 ms, setImmediate(fn)
is defined to run the function immediately after any pending events have been handled.
The new API has seen opposition in both WebKit and Gecko developers. Currently, the only browser to implement the function is Internet Explorer 10, in addition to node.js. Unfortunately, IE10 has a serious flaw that renders the API practically useless on IE10 Mobile and dubious on IE10 Desktop.
IE10 Mobile
On Windows Phone 8, instead of yielding to any currently pending events, setImmediate
yields also to any events queued later on. Essentially, it means “run this function when you have nothing else to do”. If the browser is constantly busy running other code and rendering, the function queued by setImmediate
will never be run.
As a consequence, simply adding a spin.js spinner on our login page caused Q promises to stop working completely.
The bug can be demonstrated by a simple demo page. This page has two competing function loops: runSetTimeout
which uses setTimeout(fn, 0)
and does a busy-loop for 500 ms, and runSetImmediate
that uses setImmediate
. When run on IE10 desktop or on node.js, both functions are run alternately. On WP8, runSetImmediate
is never run.
Two variations of the demo page confirm the cause: If the busy-loop is removed from runSetTimeout
, then runSetImmediate
is initially run a couple of dozen times, until updating the DOM becomes slow enough to cause it to yield forever. If the logging action is changed to replace the DOM (which is a constant-time action), then both methods are run properly on WP8 as well.
IE10 Desktop
Initially we assumed this to be a bug only in IE10 Mobile. However, when creating the test pages, I discovered conditions where runSetImmediate
fails to run on IE10 Desktop as well.
The problem can be reproduced by opening the second test page, starting the test and letting it run for a short while (presumably until the DOM manipulation is sufficiently slow). Calling of runSetImmediate
stops when either a) moving the mouse cursor, or b) placing the mouse cursor on top of the changing log lines.
Evidently, the combination of a setTimeout(fn, 0)
loop, DOM manipulation and DOM events being fired is enough to break setImmediate
even on IE10 Desktop.
It seems unlikely that this would cause any actual problems in IE10 Desktop, though it might have a performance effect on setImmediate
. In some rare cases, though, it might cause mysterious lock-ups with callbacks not being properly called.
MessageChannel & postMessage
After identifying that setImmediate
was the cause of our woes, we tried forcing Q promises to use an alternative method for waiting until the next tick. After setImmediate
, the next fallback is to use MessageChannel
. Unfortunately, this failed to work as well.
This is demonstrated by a page using Q promises. Unless you disable both setImmediate
and MessageChannel
, the same bugs appear.
We assume that MessageChannel
uses the same internal mechanism as setImmediate
, and thus suffers from the same issues.
Update: postMessage
also suffers from the same bug. Seems like there’s no fully functional zero-time timeout on IE10.
Update 2: onreadystatechange
looks like a functioning way of getting zero-delay asynchronous callbacks on IE10. On IE10 Mobile onreadystatechange
and setTimeout
are called alternately, on IE10 Desktop onreadystatechange
pre-empts setTimeout
so the latter is never called. Therefore it seems that on IE10 Desktop onreadystatechange
is called at the end of the current tick.
The workaround
Fortunately, it’s simple to force libraries not to use the broken APIs:
if (navigator && navigator.appName == 'Microsoft Internet Explorer') { window.setImmediate = undefined; window.MessageChannel = undefined; }
I’m unaware of any better way of detecting a broken setImmediate
implementation than targeting the browser. Of course, you’re less fortunate if you would actually like to use MessageChannel
on WP8.
We have reported the bug to Microsoft and various promise library maintainers.
I’d like to thank our development team — Tomas Östman, Jussi Seppälä and Kalle Korpiaho — for their efforts in deciphering this bug.
