3

I'm studying the Promise() constructor, and I noticed something unexpected for my me.

console.log('first');
const promise1 = new Promise((resolve, reject) => {
  console.log('inside executor');
  let what = 1
  console.log(what());
  console.log('not reached');
  resolve('Hi Guys!');
});
console.log('continues'); // why does it continue?

Output:

first
inside executor
continues // whaaaaaaaaaaaaaaaaaaaaaaaat?????????
index.js:5 Uncaught (in promise) TypeError: what is not a function
  at index.js:5:15
  at new Promise (<anonymous>)

Expected output (I expect this output as the executor runs synchronously):

first
inside executor
index.js:5 Uncaught (in promise) TypeError: what is not a function
      at index.js:5:15
      at new Promise (<anonymous>)

The executor the constructor is said to run synchronously, so:

why does it log continues if it should stop the execution of the script after each exception (after console.log(what();)?

I understand that I should use e.g. catch() for the promise rejection but that is not the main point of the question.

Barmar
  • 741,623
  • 53
  • 500
  • 612
gus
  • 123
  • 5
  • Because promises are asynchronous, and although yours here resolves immediately, it is still sent to the end of the execution queue. Synchronize it by placing an `await` before it and you'll see that "continues" won't show up – M0nst3R Jun 09 '23 at 19:58
  • The constructor **takes the callback as a parameter**. The callback itself does not actually execute until the promise starts resolving. The part that runs synchronously is the constructor itself (which probably just stores the callback in the object somewhere) – apokryfos Jun 09 '23 at 20:03
  • @apokryfos But it DID start executing, since it logged `inside executor`. – Barmar Jun 09 '23 at 20:05
  • @GhassenLouhaichi But if it was sent to the end of the queue, why did it log `inside executor` first? Only the exception was delayed. – Barmar Jun 09 '23 at 20:07
  • @Barmar is right guys, it has been said that the callback is executed *synchronously* [synchronously](https://stackoverflow.com/questions/29963129/is-the-promise-constructor-callback-executed-asynchronously) (see last answer of that question) – gus Jun 09 '23 at 20:08
  • Sorry, you are right. The callback is called synchronously. This is more likely to do with how unhandled rejections (or possibly even handled rejections) are managed by the runtime. By the looks of it they are handled outside the main execution flow. It's probably up to you to catch them if you want to interrupt your normal execution flow – apokryfos Jun 09 '23 at 20:10
  • 1
    **Any errors thrown in the executor will cause the promise to be rejected**. So the promise catches the error and calls `reject`. That happens asynchronously. – Barmar Jun 09 '23 at 20:13
  • Do we have resources that confirm the callback is run synchronously? Because this example does suggest that it is run asynchronously (in the next event cycle). At which point it started running line by line until the line that threw. – M0nst3R Jun 09 '23 at 20:13
  • @GhassenLouhaichi The `inside executor` log message is the proof. – Barmar Jun 09 '23 at 20:13
  • @apokryfos, that's the main point, to know what's going on? – gus Jun 09 '23 at 20:14
  • @Barmar oh right, guess I'm blind. – M0nst3R Jun 09 '23 at 20:15
  • ok from a more abstract point of view. You create a promise, it starts getting resolved synchronously and then it becomes settled as rejected. The code then continues. If you describe it that way there's nothing wrong with the code. I don't think there is any expectation that a rejected promise interrupts the normal code flow – apokryfos Jun 09 '23 at 20:23
  • @apokryfos but the code continued before it settled as rejected. Are you saying the promise start synchronously but ends asynchronously? Unless it is synchronous but the error handling is asynchronous like you said earlier. – M0nst3R Jun 09 '23 at 20:33
  • "*Any errors thrown in the executor will cause the promise to be rejected. So the promise....*" - We can say that 1) the `executor `of the constructor is executed *synchronously*, 2) the constructor "remembers" what happened in that `executor` to later (*asynchronously*) *reject* or *resolve* the promise according to the code of the `executor` that was previously executed (*synchronously*), right? @Barmar – gus Jun 09 '23 at 20:47
  • 1
    I think it's something like that, but I don't know where it's specified. – Barmar Jun 09 '23 at 20:49
  • "*Any errors thrown in the executor will cause the promise to be rejected, and the return value will be neglected.*" - But, this you mention you got from MDN, right? @Barmar – gus Jun 09 '23 at 20:53
  • 1
    Yes, that's where it came from. But it doesn't say that the rejection happens asynchronously. – Barmar Jun 09 '23 at 20:54
  • Exactly @Barmar, it would be great if someone else would join the question for feedback. At the moment, I think our theories are the most reasonable. I also believe it is rejected asynchronously, **the order of the `log`s says a lot**. – gus Jun 09 '23 at 20:58
  • 1
    The rejection seems to happen synchronously as well but all promise handling seems to be asynchronous. e.g. [here](https://jsfiddle.net/4Lposmkg/) the `.catch` callback is asynchronous. It can be assumed that the handler for unhandled rejections is also asynchronous. Same seems to happen for resolved promise `.then` handlers e.g. [here](https://jsfiddle.net/4Lposmkg/1/) – apokryfos Jun 09 '23 at 21:36
  • 1
    That's exactly what I was left thinking, the *rejection/resolution* happens *synchronously* **because the execution of the callback passed to `Promise()` is synchronously**, and as **we know that the callbacks of a `then()`/`catch()`/`finally()` are executed asynchronously** we will SEE the *rejection/resolution* also *asynchronously* (after '`first`', '`inside executor`', '`continues'`), as we don't capture the rejection the same thing happens, the unhandled rejection happens *asynchronously*, **I think it's a mere deduction as you put it** @apokryfos – gus Jun 09 '23 at 22:35
  • what do you think about my last deduction (my last comment) @Barmar? – gus Jun 09 '23 at 22:39
  • 1
    Sorry, I don't really know all the details of promises well enough to be able to judge it. I found your result just as surprising as you did. – Barmar Jun 09 '23 at 22:42
  • I really appreciate your sincerity. Still, your comments are and were very helpful :) – gus Jun 09 '23 at 22:47

1 Answers1

4

why does it log "continues" if it should stop the execution of the script after each exception (after console.log(what();))?

The error that occurs in the callback function will be caught by the internal promise implementation (which is the part that called your callback), and will set the promise's state to rejected. The execution of the callback really is interrupted as you would expect from a runtime error. But as the error is caught (handled), the caller will get the promise object back from the new Promise() call, and execution can happily continue and print "continues".

See the ECMAScript specification, at The Promise Constructor where executor is your callback function that triggers an error:

  1. Let completion be Completion(Call(executor, undefined, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »)).
  2. If completion is an abrupt completion, then
    a. Perform ? Call(resolvingFunctions.[[Reject]], undefined, « completion.[[Value]] »).
  3. Return promise.

The handling of an error happens in step 10: note how the procedure does not propagate that error further, but registers a rejection and continues with step 11, which is a normal completion.

We can imagine the internal promise constructor code to look somewhat like this (if it would be written in JavaScript), but simplified:

class Promise {
    #state
    #resolvedValue
    #customers

    constructor(executor) {
        // ... initialisation code ...
        this.#state = "pending";
        this.#customers = []; // Any `then`, `catch` calls would populate this
        //       ...
        try {
             executor((value) => this.#resolve(value), 
                      (reason) => this.#reject(reason));
        } catch(err) {
             // Absorb the error, and allow execution to continue
             this.#reject(err); 
        }
    }

    #reject(reason) {
        if (this.#state !== "pending") return; // Ignore
        this.#state = "rejected";
        this.#resolvedValue = reason;
        // Queue the asynchronous calls of any then/catch callbacks 
        //    if they exist (registered in this.#customers)
        this.#broadcast(); // I omit the implementation
    }

    // ... define #resolve, then, catch, #broadcast, ...
    // ...
}
trincot
  • 317,000
  • 35
  • 244
  • 286
  • So, the job of the `Promise()` `executor` is simply to "register-mark" the promise *synchronously* as rejected or resolved as appropriate **so as not to affect the next synchronous code**, once the synchronous code (the main JavaScript thread) is finished it now proceeds to "actually" reject/resolve the promise as expected (already asynchronously), right? @trincot – gus Jun 10 '23 at 12:04
  • 1
    What you call the "actually reject/solve", is really informing any "listeners" about the already changed state. We can consider that the promise really is rejected synchronously, but with promises it is only possible to *learn* about a state-change *asynchronously*. So that is something that only happens when the current code has run to completion, leaving the call stack empty, and then the engine will check its queues and will execute any `then`/`catch` callbacks that are pending to inform about the change in state. – trincot Jun 10 '23 at 12:17
  • "*We can consider that the promise really is **rejected synchronously**, but with promises it is only possible to learn about a **state-change asynchronously.***" - This is worth gold. So, if there is no problem and `resolve()` was called to resolve the promise, it would also resolve *synchronously*, right? @trincot – gus Jun 10 '23 at 13:10
  • 1
    Right, but with JavaScript code alone it is not possible to prove that, as we don't have synchronous access to the state of a promise object. Console implementations like Chrome's dev tools *are* able to show its state (just print the promise object to the console). This may be interesting to observe, but anyway, it is a detail that you cannot detect in JavaScript code. We can only listen to what a `then` or `catch` callback gets as argument (or the equivalent with `await`). – trincot Jun 10 '23 at 13:20
  • "*Any errors thrown in the executor will cause the promise to be rejected, **and the return value will be neglected***" - In the MDN documentation it says that, what does it mean that the return value will be "neglected"? [here, in the parameters section](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise#parameters) @trincot – gus Jun 10 '23 at 13:28
  • 1
    It means that if you have a `return 1234` in your constructor callback, that return value will be ignored. It is a statement that should be read independently of the statement concerning errors. They just list two behaviours they want to highlight: one concerning errors and another concerning returning a value. – trincot Jun 10 '23 at 18:19
  • Perfect @trincot, I thought it was directly related to the errors. Ready, marked as best answer, this last doubt was necessary. – gus Jun 11 '23 at 00:47