Order of Execution of JavaScript Promises (With Examples)
Confused about the order in which JavaScript promises execute? I was too. Working through some examples and referencing the JavaScript spec helped better my understanding — hopefully it can do the same for you.
Promise States
Before we dive into the examples, let’s review some helpful background knowledge.
A promise can be in one of three mutually exclusive states: fulfilled, rejected, or pending. Here is how spec 26.6 defines these states.
- A promise
p
is fulfilled ifp.then(f, r)
will immediately enqueue a Job to call the functionf
. - A promise
p
is rejected ifp.then(f, r)
will immediately enqueue a Job to call the functionr
. - A promise is pending if it is neither fulfilled nor rejected.
There are two more terms to be aware of: settled and resolved.
- A promise is said to be settled if it is not pending, i.e. if it is either fulfilled or rejected.
- A promise is resolved if it is settled or if it has been “locked in” to match the state of another promise. Attempting to resolve or reject a resolved promise has no effect. A promise is unresolved if it is not resolved. An unresolved promise is always in the pending state. A resolved promise may be pending, fulfilled or rejected.
Rules
We’ll go over things in more detail below, but here are some high-level rules that should be helpful when trying to understand promises. I recommend reading through the examples while referring to these rules, as opposed to trying to understand these rules thoroughly before going through the examples.
- A promise’s executor function runs synchronously.
- Calling
Promise.prototype.then()
on a fulfilled promise adds a job to the job queue (see spec 8.4, spec 26.6). Promise.prototype.then()
returns a pending promise. The promise gets fulfilled (or rejected) when the job it enqueued runs (see spec 26.6.5.4)- Jobs are run in the order they are enqueued.
- Jobs are only run when “there is no running execution context and the execution context stack is empty,” e.g. once all the synchronous code in a script has finished (see spec 8.4).
- You can think of
await
in terms ofPromise.prototype.then()
(see Example #5 and Example #6, see this StackOverflow post).
Example #1 — A simple introduction
Code
getFulfilledPromise("foo")
.then(logThen.bind(null, "1"))
.then(logThen.bind(null, "2"));console.log("After creating promise");
Logs
Promise executor, result will be foo
After creating promise
[1] Promise then, result = foo
[2] Promise then, result = foo
Explanation
We can understand this using rules #1, #2, #4, and #5.
Each call to .then()
adds a job to the queue (if we want to be really specific, this is called the “microtask queue”, see here for more details). However, these jobs only run after the script finishes, which is why After creating promise
gets logged before the last two lines.
Example #2 — Calling .then() on a fulfilled promise
Code
import {
addLoggingToPromiseThen,
getFulfilledPromise,
logThen,
} from "../util.js";addLoggingToPromiseThen();const prom1 = getFulfilledPromise("prom1");
const prom2 = getFulfilledPromise("prom2");prom2.then(logThen.bind(null, "1"));
prom2.then(logThen.bind(null, "2"));prom1.then(logThen.bind(null, "3"));
prom1.then(logThen.bind(null, "4"));
Logs
Promise executor, result will be prom1
Promise executor, result will be prom2
In Promise.prototype.then Promise { 'prom2' }
In Promise.prototype.then Promise { 'prom2' }
In Promise.prototype.then Promise { 'prom1' }
In Promise.prototype.then Promise { 'prom1' }
[1] Promise then, result = prom2
[2] Promise then, result = prom2
[3] Promise then, result = prom1
[4] Promise then, result = prom1
Explanation
We can understand this using rules #1, #2, #4, and #5 (the same rules as the first example).
Both prom1
and prom2
are fulfilled. We know from spec 26.6 that calling p.then(f, r)
on a fulfilled promise p
will “immediately enqueue a Job to call the function f
.” This means that the calls to logThen
will be put into the job queue (see spec 8.4 for more info about the job queue) in the same order as they appear in the script. Finally, since jobs run in the same order as they were enqueued (see spec 8.4.4), the logs appear in sequential order.
Example #3 — Interleaved execution
Code
import {
addLoggingToPromiseThen,
getFulfilledPromise,
logThen,
} from "../util.js";addLoggingToPromiseThen();const prom1 = getFulfilledPromise("prom1");
const prom2 = prom1.then(logThen.bind(null, "1"));
const prom3 = prom2.then(logThen.bind(null, "2"));const prom4 = getFulfilledPromise("prom2");
const prom5 = prom4.then(logThen.bind(null, "3"));
const prom6 = prom5.then(logThen.bind(null, "4"));
Logs
Promise executor, result will be prom1
In Promise.prototype.then Promise { 'prom1' }
In Promise.prototype.then Promise { <pending> }
Promise executor, result will be prom2
In Promise.prototype.then Promise { 'prom2' }
In Promise.prototype.then Promise { <pending> }
[1] Promise then, result = prom1
[3] Promise then, result = prom2
[2] Promise then, result = prom1
[4] Promise then, result = prom2
Explanation
We can understand this using rules #1, #2, #3, #4, and #5. This one will be a bit more tricky because of rule #3…
First, note that getFulfilledPromise("prom1").then(...)
does not return a fulfilled promise. We can confirm this by looking at the logs from Promise.prototype.then
— only the initial call to then
for each promise logs a fulfilled promise.
So, let’s go through this line-by-line and see what happens.
const prom1 = getFulfilledPromise("prom1");
. This just gets a fulfilled promise.const prom2 = prom1.then(logThen.bind(null, "1"));
. Here,.then()
is called on a fulfilled promise, which means that the corresponding job gets immediately enqueued (we also saw this in the last example). The call returns a pending promise that will get fulfilled when the enqueued job runs. That is, the job will run ouronFulfilled
callback, which islogThen.bind(null, "1")
, and it will fulfillprom2
. Note that jobs will only start running after the script completes (spec 8.4)! See spec 26.6.5.4.1 to read more about the details. The main important part is this line:
“Perform HostEnqueuePromiseJob(fulfillJob.[[Job]], fulfillJob.[[Realm]]).”
At this point, here is the state of things."1"
is shorthand for “the job that runslogThen.bind(null, "1")
.job_queue = ["1"]
const prom3 = prom2.then(logThen.bind(null, “2”));
. Here,.then()
is called on a pending promise. Spec 26.6.5.4.1 describes the behavior in this scenario. Here’s the relevant line:
“Append fulfillReaction as the last element of the List that is promise.[[PromiseFulfillReactions]].”
In simple terms, this makes it so that whenprom2
gets fulfilled,logThen.bind(null, "2"))
will be called.
At this point, here is the state of things.job_queue = ["1"]
prom2_fulfill_reactions = ["2"]const prom4 = getFulfilledPromise("prom2");
. This is basically the same as #1, it just gets a fulfilled promise.const prom5 = prom4.then(logThen.bind(null, "3"));
. This is similar to #2. Since.then()
is called on a fulfilled promise, the corresponding job gets immediately enqueued. Just as before, the job will run our callback and fulfill the promise returned by the call to.then()
(which isprom5
in this case).
At this point, here is the state of things.job_queue = ["1", "3"]
prom2_fulfill_reactions = ["2"]const prom6 = prom5.then(logThen.bind(null, "4"));
. This is basically the same as #3. However, this time, we add a reaction to the list forprom5
, notprom2
.
At this point, here is the state of things.job_queue = ["1", "3"]
prom2_fulfill_reactions = ["2"]
prom5_fulfill_reactions = ["4"]
Alright, that’s it for the execution of the script itself. After the script runs, jobs from the job queue will start running. Here’s how that goes.
Jobs get run in the order they were enqueued, so the job we’ve named "1"
will execute first. This explains why [1] Promise then, result = prom1
is the first line that’s logged. Remember that this job not only runs our callback, but also fulfills prom2
. If we take a look at spec 26.6.1.4 and spec 26.6.1.8, we can see that fulfilling a promise enqueues a job for each element in its “fulfill reactions” list. So, after this job runs, here is the state of things.
job_queue = ["3", "2"]
prom5_fulfill_reactions = ["4"]
Job "1"
gets run and popped from the queue and job "2"
gets enqueued.
The next job in the queue is "3"
, so that’s the next job that will execute. Just as with the first job, it will run the callback (thus logging [3] Promise then, result = prom2
) and enqueue job "4"
. After this job runs, here is the state of things.
job_queue = ["2", "4"]
It should be clear what happens at this point :).
Example #4 — Interleaved execution, again
Code
import { getFulfilledPromise, logThen } from "./util.js";const main = () => {
new Promise((resolve, reject) => {
console.log("Start main");
resolve();
})
.then(() => {
console.log("Intermediate main");
})
.then(() => {
console.log("End main");
});
};getFulfilledPromise("outer")
.then(logThen.bind(null, "1"))
.then(logThen.bind(null, "2"));
main();
Logs
Promise executor, result will be outer
Start main
[1] Promise then, result = outer
Intermediate main
[2] Promise then, result = outer
End main
Explanation
We can use the same exact reasoning as Example #3 in order to understand this example. The differences are just syntactical:
- Instead of assigning all of the intermediate promises to variables, the
.then()
calls are chained inline. - One of the promises is created in a function named
main
.
Example #5 — Interleaved execution, with await
Code
const main = async () => {
console.log("Start main");
await null;
console.log("Intermediate main");
await null;
console.log("End main");
};getFulfilledPromise("outer")
.then(logThen.bind(null, "1"))
.then(logThen.bind(null, "2"));
main();
Logs
Promise executor, result will be outer
Start main
[1] Promise then, result = outer
Intermediate main
[2] Promise then, result = outer
End main
Explanation
This example involves all the rules!
The main reason I included Example #4 was to set up this example :). Spec 6.2.3.1 tells us how await
works; it’s fairly complicated to read through it all, but it boils down to the fact that await
is mainly just syntactic sugar. For example, these two code blocks are analogous (they are not exactly equivalent, e.g. when using await
you must use try
/catch
to handle rejected promises).
await foo(); // foo is an async function that returns a promise
console.log("hello");foo().then(() => {
console.log("hello");
});
Credit for this example goes to jfriend00, see the original post here.
This means if we write main
like this, it’s functionally equivalent.
const main = () => {
console.log("Start main");
new Promise((resolve) => {
resolve();
}).then(() => {
console.log("Intermediate main");
new Promise((resolve) => {
resolve();
}).then(() => {
console.log("End main");
});
});
};
Further, instead of nesting the promises, we can flatten them out. If we do that, then the code looks exactly like Example #3! So if we understand how Promise.prototype.then()
works, we should also be able to understand await
.
Example #6 — Await, again
Code
const func1 = async () => {
console.log("Start func1");
await null;
console.log("Intermediate func1, before calling func2");
func2();
console.log("Intermediate func1, after calling func2");
await null;
console.log("End func1");
};const func2 = async () => {
console.log("Start func2");
await null;
console.log("Intermediate func2");
await null;
console.log("End func2");
};func1();
Logs
Start func1
Intermediate func1, before calling func2
Start func2
Intermediate func1, after calling func2
Intermediate func2
End func1
End func2
Explanation
Again, this example involves all the rules.
Example #5 was our first taste of async
/await
— this example will help us solidify the same concepts. Before, we said that these two code blocks are analogous:
await foo(); // foo is an async function that returns a promise
console.log("hello");foo().then(() => {
console.log("hello");
});
This means that you can think of of await
in terms of Promise.prototype.then()
, which is quite nice. This is actually all you need to know to understand how this example works. That is, you can rewrite this example like so.
const func1Alt = () => {
new Promise((resolve) => {
console.log("Start func1");
resolve();
})
.then(() => {
console.log("Intermediate func1, before calling func2");
func2Alt();
console.log("Intermediate func1, after calling func2");
})
.then(() => {
console.log("End func1");
});
};const func2Alt = () => {
new Promise((resolve) => {
console.log("Start func2");
resolve();
})
.then(() => {
console.log("Intermediate func2");
})
.then(() => {
console.log("End func2");
});
};func1Alt();
The output is be exactly the same, and understanding this just requires understanding Promise.prototype.then()
. Nice!
Some resources explain await
differently, For example, MDN says the following:
An
await
can split execution flow, allowing the caller of theawait
's function to resume execution before the deferred continuation of theawait
's function. After theawait
defers the continuation of its function, if this is the firstawait
executed by the function, immediate execution also continues by returning to the function's caller a pendingPromise
for the completion of theawait
's function and resuming execution of that caller.
In the context of our example, this says that when the first await
in func2
is hit, control flow returns to func1
(meaning Intermediate func1, after calling func2
will be logged), and the call to func2()
in func1
returns a pending Promise
. This makes sense, and it can be helpful to view the first await
as returning control flow back to the caller, but I don’t think this is the best way to think about. Thinking about await
in terms of Promise.prototype.then()
is much more general, and lets you understand complicated scenarios fairly easily.
If you really want to know exactly how things are supposed to work, take a look at spec 6.2.3.1.
Next Time
That’s it for this post! It was quite long, but hopefully these examples are useful. Next time, I’ll cover how setTimeout
fits into the picture, and hopefully touch on microtasks vs. macrotasks.
Sources
- https://stackoverflow.com/questions/46408228/es6-promise-execution-order-for-returned-values
- https://stackoverflow.com/questions/36870467/what-is-the-order-of-execution-in-javascript-promises
- https://stackoverflow.com/questions/63862842/in-javascript-in-what-order-are-then-handlers-executed
- https://javascript.info/microtask-queue