This is a fantastic post on the blink-dev discussion list by Chrome dev James Robinson describing how setTimeout’s timer clamping works. James argues that
setTimeout(fn, 0) behaves exactly as
setImmediate(fn) would except in excessively-nested timers.
Nearly everything I’ve read starts off with an incorrect idea about how timer clamping works or why it’s in place. Â The way timer clamping works  is every task has an associated timer nesting level. Â If the task originates from a setTimeout() or setInterval() call, the nesting level is one greater than the nesting level of the task that invoked setTimeout() or the task of the most recent iteration of that setInterval(), otherwise it’s zero. Â The 4ms clamp only applies once the nesting level is 4 or higher. Â Timers set within the context of an event handler, animation callback, or a timer that isn’t deeply nested are not subject to the clamping. Â This detail hasn’t been reflected in the HTML spec until recently (and there’s an off-by-one error in it right now), but has been true in WebKit/Blink since 2006  and in Gecko since 2009 . Â The practical effect of this is that setTimeout(…, x) means exactly what you think it would even for x in [0, 4) so long as the nesting level isn’t too high.
With a better understanding of timer clamping, let’s consider the possible use cases for scheduling asynchronous work. Â One is to time work relative to display updates. Â Obviously requestAnimationFrame() is the right API to use for animations, but it’s also the right choice for one-off uses. Â To perform work immediately before the next display – for example to batch up graphical updates as data is streamed in off the network – just make a one-off requestAnimationFrame() call. Â To perform work immediately -after- a display, just call setTimeout() from inside the requestAnimationFrame() handler. Â The nesting level is zero within a rAF() callback so the timeout parameter will not be clamped to 4.