10

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'

Wing
  • 8,438
  • 4
  • 37
  • 46
  • 3
    Promise callbacks *never* run immediately. That's part of their charm. – Bergi Jun 27 '16 at 17:04
  • The only portion of a Promise that executes immediately is the executor of the Promise constructor: `new Promise((resolve) => {...executor...}` –  Jul 02 '16 at 21:32
  • 2
    Why on earth was this sitting at -1 votes? It's a great question, and the accepted answer from @Jeff-Bowman is excellent and helps me understand A+ much better. As a result, I think I finally understand why this clause of the spec may be more than just gratuitous complication as it appears due to lack of any rationale for it whatsoever in the spec. So thanks to you both. – Don Hatch Nov 02 '16 at 04:08

3 Answers3

7

The relevant specs are here:

  1. 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.

  2. 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:

    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»).

    2. 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.
Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
  • 1
    I imagine an important consequence of the second bullet "You'll never see multiple `then` or `catch` handlers on the same stack" is that it can avoid hitting the recursion limit in some cases. Maybe worth stating, if that's the case. – Don Hatch Nov 02 '16 at 04:19
2

This is not possible due to the way promises works. Even promises which immediate resolves runs for the next tick, what you want is sync functions not promises.

See this for example:

setTimeout(function() {
    console.log("test");
}, 0);


console.log("test2");

It is impossible to print test before test2 without removing the setTimeout function, because even if the wait parameter is 0, it will run for the next tick which means that it will run when all sync code has run.

2

I really don't mean to be blunt, but it is because that is the way the spec says that they work. If you have a need for a piece of code to run at a certain point after a code within a promise finishes, then you should utilize the promise chain. Once you introduce asynchronous code into the mix, it is a bad idea to try and mix it with dependent, synchronous, code.

Daisy chain promises whenever you need things to depend on asynchronous code:

function foo() {
    console.log('foo');
    return Promise.resolve();
}

function callback() {
    console.log('callback');
}

function consoler() {
    console.log('bar');
}

foo().then(callback).then(consoler);

Produces:

foo
callback
bar
zero298
  • 25,467
  • 10
  • 75
  • 100
  • `then` callbacks actually don't even need to return promises. – Bergi Jun 27 '16 at 17:13
  • "I really don't mean to be blunt, but it is because that is the way the spec says that they work." Well of course. I think the answer I'm looking for are ones that explains **how** (and maybe why) they work rather than just say 'the spec says it works like that so it does' or are ones that start suggesting alternatives. I'm fully aware on how to use Promises, it was just playing around I came across this case where the logic of the intention did not match the expectations. Thanks for your input. – Wing Jun 27 '16 at 19:28