2

In this code:

setTimeout(() => console.log('C'), 100); 
setTimeout(() => console.log('D'), 0);
const t1 = new Date();
while (new Date() - t1 < 500) {/* block for 500ms */}

This will log D -> C, but why?

Why wouldn't it log C -> D? As setTimeout for C is pushed to event loop queue first and 500ms have passed which is > 100 ms. So, now both setTimeouts are ready to be executed. so, it should execute the `C` one as it was pushed to the queue earlier?

3
  • I'm confused as to how you could ever expect C would be shown first when it has a 100ms longer delay on it...? Commented May 7, 2024 at 14:47
  • A delay of zero means meaning execute "immediately", or more accurately, the next event cycle. As such it is queued up first, try adding setTimeout(() => console.log('E'), 1); one with a delay of 1. I'll bet anything that it's D -> E -> C. Docs are here Commented May 7, 2024 at 15:56
  • 1
    "setTimeout for C is pushed to event loop queue first" - no it isn't? Commented May 7, 2024 at 17:54

3 Answers 3

3

It depends on implementations (setTimeout is not part of JavaScript and may follow the specification described by the HTML standard, as with Deno, or may deviate from it, as with NodeJS).

In our case, setTimeout registers two operations that will be executed after the 500ms blocking task, when waking up the scheduler looks for the first task to execute (if it's not done on insert like here in the Deno implementation for example).

Edit: timestamp is associated with the task, so even if the scheduler is late it can determine which task should be executed first: the first task to execute will be the one with the lowest timestamp (t + 0 (D) < t + 100 (C)) so it will execute D first.

Sign up to request clarification or add additional context in comments.

1 Comment

Regarding the last note, it may also vary depending on the implementation. In node it seems it corresponds to the scheduled wall-clock indeed. But in Chrome for instance, if you do yet another setTimeout(() => console.log('E'), 0); after the busy loop, then it'd log D,E,C, (because they have a special case for 0 timeouts since a couple of years).
1

Javascript works with an execution stack and a parallel async queue to run its commands. Imagine you have a bunch of cards and you are ditributing them to be executed like in your code. The first one you'll wait 100ms to be put in parallel queue, the second is immediately put in parallel queue, so the second card stands in front of the first in this queue, and then you start resolving the other cards in regular stack that takes 500ms. When the regular stack finally gets a free spot, you go back to look to the parallel queue and take the front card to be executed (during its execution, regular stack is busy again, so parallel queue is put to wait again, remaining the last card). Just when this execution is finished you'll go back and take the last card from the queue (console.log('C'))

2 Comments

"parallel async queue" citation needed on the parallel part. Asynchrony != parallelism.
@JaredSmith I don't know if it's what André was referring to, but at least in HTML that how we call the magic space where async stuff are supposed to happen: html.spec.whatwg.org/multipage/#in-parallel That doesn't necessarily mean parallelism (as opposed to time-sharing) is being used, the observable result is the same: something ran while your script was running.
1

Note: This answer is for the execution model according to the HTML specification. However, the question is about .


With setTimeout(), timers are created to enqueue tasks. You call it twice, which create:

  1. A timer with timeout of 100 to enqueue a task that logs "C".
  2. A timer with timeout of 0 to enqueue a task that logs "D".

So the timer "for C" is created before the timer "for D". But timers run in parallel, and are not in a queue; instead, they enqueue their tasks into the so-called task queue. (You may have confused timers running parallely, tasks and the task queue!)

In your case:

  1. The timer "for D" enqueues its task first. The task queue now holds the task "for D".
  2. The timer "for C" enqueues its task second. The task queue now holds the tasks "for D" and "for C", in this order.

They may not have executed until now, e.g. because of someSynchronousTask() taking too long. So only once the event loop was free would the tasks of the task queue run, in this order:

  1. The task that logs D first.
  2. The task that logs C second.

This is your observed behaviour.

In conclusion: setTimeout() doesn't enqueue tasks directly, but creates timers which enqueue tasks after some time. The final order of the task queue depends on when the timers run, not when they are created.


An analogy to "issued one way, done in reverse":

Imagine you tell a friend to meet up together in 10 minutes.

Then you tell them to ask now (0 minutes) whether others would join the meet-up, too.

So your friend asks around first, then meets up in 10 minutes; even though you had told these in reverse. But according to your given times, the events were planned in this order.

7 Comments

"The timer enqueues." could be misunderstood as "the timer is enqueued", not "the timer enqueues the task".
I think this answer missed the point // Assume this takes ~500ms. Why in such a case is C still executed before D? After all, at the time the event loop checks which timers have elapsed, both had, so it could make sense that the first one that has been set would fire first. And then it would also be interesting to check what happens when calling the setTimeout(,0) after the busy loop, to see that it may not even be a wall clock thing, but actually more like a sort of queue on its own
@Kaiido The event loop doesn't check for run-out timers; it only iterates the task queue until empty. Timers fill the task queue (see Step 5), which happens in parallel to the event loop; this is unrelated to the event loop's execution. Apparently my answer isn't clear enough; do you have a suggestion where and how to clarify this?
@Kaiido I don't understand your "after the busy loop ... wall clock thing" example. Do I understand correctly that this example would be redundant when knowing that timers fill the task queue in parallel to (instead of relying on) the event loop? Do you mean that calls to setTimeout() with 0 timeout are "collected" (as if in a queue) and all run after synchronous code? Or are you asking to clarify when the event loop empties the JavaScript execution context stack, the task queue, and the microtask queue; i.e. when synchronous and asynchronous code executes?
First, here the question is about node's behavior, which does not follow the HTML specs, so using it as a source of truth doesn't seem appropriate. Then, even if it's not the event loop itself that does check which timers are to be ran the result is the same. There is a process that will periodicaly check which steps are to be run that will ultimately queue the task to execute the callbacks. This process could be blocked by user-scripts, so it could make sense that even if a timer was created with a smaller delay it could be executed after.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.