-2

Consider the following code.

(async () => {
    const values = {}
    const orders = []
    let promise = 0
    promise = new Promise(async resolve => {
        values[1] = promise
        orders.push(1)
        resolve(1)
        values[2] = promise
        orders.push(2)
        await new Promise(async r => {
            values[3] = await promise
            orders.push(3)
            setTimeout(r, 100)
        })
        values[4] = await promise
        orders.push(4)
        promise = 2
        values[5] = promise
        orders.push(5)
    })
    values[6] = await promise
    orders.push(6)
    await new Promise(resolve => setTimeout(resolve, 100)) // (*). try removing this line, result will change
    values[7] = await promise
    orders.push(7)
    console.log(values, orders)
})()

Console output:

{ '1': 0, '2': 0, '3': 0, '4': 1, '5': 2, '6': 1, '7': 1 }
[ 1, 2, 3, 6, 4, 5, 7 ]

My questions are:

  1. Is there any statement in the above snippet which is undefined behavior? If no, I am confused of the ES6 promise after checking the result.

  2. Why is values[7] 1, not 2? Update: with node v10.19.0, it is 1. With nodev14.0.0, node-v12.10.0 and browser, it is 2.

Update 2: repeating the evaluation with node v10.19.0, after 20 times of values[7] be 1, once it will be 2.

  1. If I remove the (*) line, the result changes to below. Why are 4, 5 removed?

    { '1': 0, '2': 0, '3': 0, '6': 1, '7': 1 } [ 1, 2, 3, 6, 7 ]

If you have any rule of thumb for this kind of execution, please share. The more I write JS promise code, the more I get confused.

Check for more tests here.

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Sang
  • 4,049
  • 3
  • 37
  • 47
  • First code smell. `const values = {}` and `values[1] = ...`. You are trying to treat an object like an array – Taplar May 05 '20 at 17:42
  • "Now, without executing the code, can you give a correct answer to the final value of values?" Sounds like homework. – epascarello May 05 '20 at 17:42
  • 1
    It is unclear why anyone would write code like that to begin with. – epascarello May 05 '20 at 17:43
  • @epascarello I created this test myself lol – Sang May 05 '20 at 17:46
  • @Taplar it doesn't matter. numbers can be object keys. – Sang May 05 '20 at 17:50
  • I am aware of that, but my point remains. If you are treating it like an array, you should use an array. Unless you have a reason to not do so. – Taplar May 05 '20 at 17:51
  • @epascarello actually, It happens in my real project, which made me to write this test code to confirm the language specs – Sang May 05 '20 at 17:53
  • @Taplar I did not treat `values` as an array. The indices present the code line number order, not runtime assignment order. That is why I use object by purpose, not by mistake. – Sang May 05 '20 at 17:55
  • WHAT do you not expect to work? My guess you are freaking out that promise resolve(2) is not updating for the code right after it. – epascarello May 05 '20 at 18:04
  • @epascarello values[9] should be 6, but it is 5 – Sang May 05 '20 at 18:07
  • 2
    [Never pass an `async function` as the executor to `new Promise`](https://stackoverflow.com/q/43036229/1048572)! Even if you're trying to write some scheduling puzzles. – Bergi May 05 '20 at 18:10
  • "*If you have any rule of thumb for this kind*" - the rule is not to write convoluted asynchronous code that reassigns the same variable over and over :-) – Bergi May 05 '20 at 18:12
  • @transang no it should be 5. because the `resolve(5)` updates your variable after you set it to 6. – epascarello May 05 '20 at 18:22
  • I have to rethink the meaning of *obfuscated code*... And I thought that `(![]+[])[!+[]+!+[]]+[+[]]+(![]+[])[!+[]+!+[]]` resolving to `"l0l"` is considered obfuscated... – FZs May 05 '20 at 18:31
  • Most of these are well-defined and just run in order, but the `promise = 6` assignment and the `values[9] = await promise` form a classical race condition that depends on the execution speed, accuracy, and microtask semantics of `setTimeout`. – Bergi May 05 '20 at 18:52
  • `promise` is a schizophrenic beast. Why? – Roamer-1888 May 05 '20 at 19:34
  • @epascarello promise = 6 is placed **after** resolve(5). Actually, I do not think it (promise = 6) is getting called – Sang May 05 '20 at 22:33
  • @Bergi I want something more than the link you provided. Eventhough this is an antipattern, it has a well-defined behavior, what is the expected behavior? – Sang May 05 '20 at 23:13
  • @Bergi my question is about the rule of thumb is about the rule of determining the execution order, not rule of writing code. – Sang May 05 '20 at 23:14
  • @Bergi javascript is single-thread. There is no external factor (internet, IO, ...) here. I do not think there is a race condition. – Sang May 05 '20 at 23:16
  • regarding the second question. I posted another question [here](https://stackoverflow.com/questions/61627447/what-is-the-random-factor-in-node-v10-event-loop) – Sang May 06 '20 at 04:32
  • @transang `setTimeout` *is* IO, and also its exact implementation is not specified but depends on the runtime environment (e.g. it works differently in node vs a browser). All I'm saying is that this part of the code is written in a manner that makes impossible to argue about deterministic execution order (two *independent* timeouts of similar length), which is a race condition. – Bergi May 06 '20 at 08:20
  • @Bergi You are not correct. [this post](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/) explains internal event loop of nodejs. setTimeout appends the callback to the timer loop with a very small (fixed) time value. – Sang May 06 '20 at 08:57
  • @transang And yet, it doesn't discuss subtle details in `setTimeout` such as [this](https://stackoverflow.com/q/61608974/1048572). And no, the event loops used by chrome and node are different! – Bergi May 06 '20 at 09:05

1 Answers1

1

If there's something you're confused about, try to distill it to just the case you are confused about, not all the other cases too, that said... my best guess to what I think you want to know:

  1. The executor function (what you pass to new Promise()) gets called immediately, synchronously.
  2. Async functions, when called, also get executed immediately. They will only pause when they hit the first await.
  3. So if you use an async function that has no await in it, the entire async function will get executed synchronously, immediately.
  4. However, even if you async function is "synchronous", it will not immediately return. Execution is synchronous, but resolving (handling .then() listeners) will be in the next tick).
Evert
  • 93,428
  • 18
  • 118
  • 189
  • does 2 hold if the waited value is not a promise. Consider 2 cases: it is a dynamic runtime-defined variable, and it is a constant non-promise variable – Sang May 05 '20 at 23:47
  • i have added test. `values[3]` is `0`, not `1`. Could you add more rules to cover this case? – Sang May 06 '20 at 00:13
  • @transang, no your question is too dense to try and interpret everything. Perhaps you can open a more specific question. You're basically asking to interpret the Promise and async/await specifications for you. – Evert May 06 '20 at 00:39
  • thanks. Can you explain why was `3` added to `orders`, before `6`? Does this contrast with your second rule? – Sang May 06 '20 at 00:55
  • I figured out the answer myself. because push(3) is scheduled (nextTick?) before push(6), it is executed first. The async function does stop at any await regardless the value of the awaited operand – Sang May 06 '20 at 03:42