Why does JavaScript Promise then handler run after other code?
ecmascript-6, es6-promise, javascript, promise
Solution
The relevant specs are here:
Promises/A+ point 2.2.4:
`onFulfilled` or `onRejected` must not be called until the execution context stack contains only platform code. [3.1].
And note 3.1 (emphasis mine):
Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that `onFulfilled` and `onRejected` execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as `setTimeout` or `setImmediate`, or with a “micro-task” mechanism such as `MutationObserver` or `process.nextTick`. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.
ECMAScript 6.0 (based on Promises/A+) is a little harder to excerpt cleanly, but `then` resolves as in section 25.4.5.3.1:
Else if the value of promise's [[PromiseState]] internal slot is `"fulfilled"`,
a. Let value be the value of promise's [[PromiseResult]] internal slot.
b. Perform EnqueueJob(`"PromiseJobs"`, PromiseReactionJob, «fulfillReaction, value»).
Else if the value of promise's [[PromiseState]] internal slot is `"rejected"`,
a. Let reason be the value of promise's [[PromiseResult]] internal slot.
b. Perform EnqueueJob(`"PromiseJobs"`, PromiseReactionJob, «rejectReaction, reason»).
And the important EnqueueJob operation is defined in section 8.4 ("Jobs and Job Queues"), featuring this in its preface (bold is mine):
Execution of a Job can be initiated only when there is no running execution context and the execution context stack is empty. [...] Once execution of a Job is initiated, the Job always executes to completion. No other Job may be initiated until the currently running Job completes.
In practice, this lets you make a few simple and consistent statements:
- You can count on `then` or `catch` (etc) to always behave asynchronously, never synchronously.
- You'll never see multiple `then` or `catch` handlers on the same stack, even if one Promise is explicitly resolved within another Promise. This also means that recursive Promise execution doesn't risk stack overflows as a normal function call might, though you can still run out of heap space if you're careless with recursive closures in a pathological case.
- Time-consuming operations queued in a `then` or `catch` handler will never block the current thread, even if the Promise is already settled, so you can queue up a number of asynchronous operations without worrying about the order or promise state.
- There will never be an enclosing `try` block outside of a `then` or `catch`, even when calling `then` on an already-settled Promise, so there's no ambiguity about whether the platform should handle a thrown exception.
Problem
I'm just trying to improve my understanding on how JavaScript Promises work. I've created the following situation: ``` LOG 'FOO' RUN CALLBACK LOGGING 'CALLBACK' LOG 'BAR' ``` Expect all functions to complete immediately (by this I mean they will not take an excessive/unknown amount of time to complete that you would use an async operation to complete) so that the above order of operations will happen in that order. You can write this in the following way: ``` function foo(cb) { // LOG 'FOO' console.log('foo'); // RUN CALLBACK cb(); } function callback() { // LOG 'CALLBACK' console.log('callback'); } foo(callback); console.log('bar'); ``` This produces the expected output according to the situation I specified at the beginning. ``` > foo > callback > bar ``` You could also write it in the following way: ``` function foo() { return new Promise((resolve) => { // LOG 'FOO' console.log('foo'); return resolve(null); }); } function callback() { // LOG 'CALLBACK' console.log('callback'); } foo().then(callback); // LOG 'BAR' console.log('bar'); ``` This situation produces the following result: ``` > foo > bar > callback ``` This is where I am unclear as I am expecting `foo` to have completed immediately so that `callback` will run and log `'callback'` before `bar` logs `'bar'`