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.