Post cover

Why Promises Are Faster Than setTimeout()?

1. The experiment

Let's try an experiment. What does execute faster: an immediately resolved promise or an immediate timeout (aka a timeout of 0 milliseconds)?


Promise.resolve(1).then(function resolve() {
console.log('Resolved!');
});
setTimeout(function timeout() {
console.log('Timed out!');
}, 0);
// logs 'Resolved!'
// logs 'Timed out!'

Promise.resolve(1) is a static function that returns an immediately resolved promise. setTimeout(callback, 0) executes the callback with a delay of 0 milliseconds.

Open the demo and check the console. You'll notice that 'Resolved!' is logged first, then 'Timeout completed!'. An immediately resolved promise is processed faster than an immediate timeout.

Might the promise process faster because the Promise.resolve(true).then(...) was called before the setTimeout(..., 0)? Fair enough question.

Let's change slighly the conditions of the experiment and call setTimeout(..., 0) first:


setTimeout(function timeout() {
console.log('Timed out!');
}, 0);
Promise.resolve(1).then(function resolve() {
console.log('Resolved!');
});
// logs 'Resolved!'
// logs 'Timed out!'

Open the demo and look at the console. Hm... the same result!

setTimeout(..., 0) is called before Promise.resolve(true).then(...). However, 'Resolved!' is still logged before 'Timed out!'.

The experiment has demonstrated that an immediately resolved promise is processed before an immediate timeout. The big question is... why?

2. The event loop

The questions related to asynchronous JavaScript can be answered by investigating the event loop. Let's recall the main components of how asynchronous JavaScript works.

Note: if you aren't familiar with the event loop, I recommend watching this video before reading further.

Event Loop Empty

The call stack is a LIFO (Last In, First Out) structure that stores the execution context created during the code execution. In simple words, the call stack executes the functions.

Web APIs is the place the async operations (fetch requests, promises, timers) with their callbacks are waiting to complete.

The task queue (also named macrostasks) is a FIFO (First In, First Out) structure that holds the callbacks of async operations that are ready to be executed. For example, the callback of a timed out setTimeout() — ready to be executed — is enqueued in the task queue.

The job queue (also named microtasks) is a FIFO (First In, First Out) structure that holds the callbacks of promises that are ready to be executed. For example, the resolve or reject callbacks of a fulfilled promise are enqueued in the job queue.

Finally, the event loop permanently monitors whether the call stack is empty. If the call stack is empty, the event loop looks into the job queue or task queue, and dequeues any callback ready to be executed into the call stack.

3. Job queue vs task queue

Let's look again at the experiment from the event loop perspective. I'll make a step by step analysis of the code execution.

A) The call stack executes setTimeout(..., 0) and schedules a timer. timeout() callback is stored in Web APIs:


setTimeout(function timeout() {
console.log('Timed out!');
}, 0);
Promise.resolve(1).then(function resolve() {
console.log('Resolved!');
});

Event Loop

B) The call stack executes Promise.resolve(true).then(resolve) and schedules a promise resolution. resolved() callback is stored in Web APIs:


setTimeout(function timeout() {
console.log('Timed out!');
}, 0);
Promise.resolve(1).then(function resolve() {
console.log('Resolved!');
});

Event Loop

C) The promise is resolved immediately, as well the timer is timed out immediately. Thus the timer callback timeout() is enqueued to task queue, the promise callback resolve() is enqueued to job queue:

Event Loop

D) Now's the interesting part: the event loop priorities dequeueing jobs over tasks. The event loop dequeues the promise callback resolve() from the job queue and puts it into the call stack. Then the call stack executes the promise callback resolve():


setTimeout(function timeout() {
console.log('Timed out!');
}, 0);
Promise.resolve(1).then(function resolve() {
console.log('Resolved!');
});

'Resolved!' is logged to console.

Event Loop

E) Finally, the event loop dequeues the timer callback timeout() from the task queue into the call stack. Then the call stack executes the timer callback timeout():


setTimeout(function timeout() {
console.log('Timed out!');
}, 0);
Promise.resolve(1).then(function resolve() {
console.log('Resolved!');
});

'Timed out!' is logged to console.

Event Loop

The call stack is empty. The script execution has been completed.

4. Summary

Why an immediately resolved promise is processed faster than an immediate timer?

Because of the event loop priorities dequeuing jobs from the job queue (which stores the fulfilled promises' callbacks) over the tasks from the task queue (which stores timed out setTimeout() callbacks).

Like the post? Please share!

Dmitri Pavlutin

About Dmitri Pavlutin

Software developer and sometimes writer. My daily routine consists of (but not limited to) drinking coffee, coding, writing, overcoming boredom 😉. Living in the sunny Barcelona. 🇪🇸