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'
The relevant specs are here:
onFulfilled
oronRejected
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
andonRejected
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 assetTimeout
orsetImmediate
, or with a “micro-task” mechanism such asMutationObserver
orprocess.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:
then
or catch
(etc) to always behave asynchronously, never synchronously.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.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.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.