I was looking into async behaviour in JS and it was going well for the most part. I understand the synchronous way of executing code, the single thread of JS and how callbacks such as the one inside setTimeout will be timed by the Web browser API, and later on added to the task queue.
The event loop will constantly check the call stack, and only when it is empty (all sync code has executed), it will take functions that have been queued in the task queue. Pushes them back to the call stack and they are executed.
This is pretty straight forward and is the reason why following code:
console.log('start');
setTimeout(() => console.log('timeout'), 0);
console.log('end');
Will output start, end, timeout.
Now when I started reading about promises, I understood that they have higher priority than regular async code such as timeout, interval, eventlistener and instead will get placed in the job queue/microtask queue. The event loop will first prioritize that queue and run all jobs until exhaustion, before moving on to the task queue.
This still makes sense and can be seen by running:
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');
This outputs start, end, promise, timeout. Synchronous code executes, the then callback gets pushed to the stack from the microtask queue and executed, setTimeout callback task from the task queue gets pushed and executed. All good so far.
I can wrap my head around the example above where the promise gets resolved immediately and synchronously, as told by the official documentation. The same would happen if we were to create a promise with the new keyword and provide an executor function. That executor function will execute synchronously and resolve the function. So when then is encountered, it can just run asynchronously on the resolved promise.
console.log('start');
const p1 = new Promise(resolve => {
console.log('promise 1 log');
resolve('promise 1');
});
p1.then(msg => console.log(msg));
console.log('end');
The snippet above will output start, promise 1 log, end, promise 1 proving that the executor runs synchronously.
And this is where i get confused with promises, let's say we have the following code:
console.log('start');
const p1 = new Promise(resolve => {
console.log('promise 1 log');
setTimeout(() => {
resolve('promise 1');
}, 0);
});
p1.then(msg => console.log(msg));
console.log('end');
This will result in start, promise 1 log, end, promise 1. If the executor function gets executed right away, that means that the setTimeout within it will get put on the task queue for later execution. To my understanding, this means the promise is still pending right now. We get to the then method and the callback within it. This will be put in the job queue. the rest of the synchronous code is executed and we now have the empty call stack.
To my understanding, the promise callback will have the priority now but how can it execute with the still unresolved promised? The promise should only resolve after the setTimeout within it is executed, which still lies inside the task queue. I have heard, without any extra clarification that then will only run if the promise is resolved, and from my output i can see that's true, but i do not understand how that would work in this case. The only thing i can think of is an exception or something similar, and a task queue task getting the priority before the microtask.
This ended up being long so i appreciate anyone taking the time to read and answer this. I would love to understand the task queue, job queue and event loop better so do not hesitate posting a detailed answer! Thank you in advance.
We get to the then method and the callback within it. This will be put in the job queue.
No, calling then doesn't put anything in the job queue immediately if the promise is still pending. The callback will be installed on the promise for execution later when the promise is fulfilled, just like an event handler. Only when you call resolve(), it actually puts it in the job queue.
This works just like the setTimeout, where you wrote "[the] callback […] will be timed by the Web browser API, and later on added to the task queue" - it doesn't immediately queue a task that somehow waits, but it waits and then queues a task to execute the callback.
... the promise callback will have the priority now ...
Tasks in the microtask queue are given priority over those in the task queue only when they exist.
In the example :
No microtask is queued until after the setTimout() task has resolved the Promise.
The task and microtask are not in competition. They are sequential.
Delays imposed by the task queue and microtask queue (in that order) are additive.
... but how can it execute with the still unresolved promised?
It doesn't. The .then() callback will execute only after the promise is fulfilled, and that fulfillment is dependent on a task placed in the task queue by setTimeout() (even with a delay of zero).
JS engine has got 1 call stack, macro task queue, micro task queue and web api's. More about basic concept: https://stackoverflow.com/a/30910084/1779091
In case of Promise, code inside the promise will run and when resolve is called, then the callback gets added into the micro queue.
Whereas setTimeout runs in the web api and once it completes, it puts the callback into the macro queue.
console.log('start');
setTimeout(() => console.log('timeout'), 0);
console.log('end');
Print start
Call the setTimeout web api and pass the call back into it
At this point the setTimeout may or may not have already completed. Whenever timer is exhausted, the callback will be put into the macro queue
Print end
Nothing left to execute, so check if the queue has something. This will output the timeout.
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');
Print start
Call the setTimeout web api and pass the call back into it
At this point the setTimeout may or may not have already completed. Whenever timer is exhausted, the callback will be put into the macro queue
Promise is resolved which puts the callback (the thing after .then) into the micro task queue
Print end
Nothing left to execute, so check if the queue has something. Micro task queue has higher priority then macro task queue. So 1st it will take the callback from micro task into the call stack and print promise and then take the callback from the macro task queue into the call stack and print timeout.
console.log('start');
const p1 = new Promise(resolve => {
console.log('promise 1 log');
resolve('promise 1');
});
p1.then(msg => console.log(msg));
console.log('end');
Print start
Create the promise and assign it to p1
Run the p1 which prints promise 1 log, then resolve which puts the callback (the thing after .then) into the micro task queue
Print end
Nothing left to execute, so check if the queue has something. The callback from the Micro task is put into the stack and it will print promise 1
console.log('start');
const p1 = new Promise(resolve => {
console.log('promise 1 log');
setTimeout(() => {
resolve('promise 1');
}, 0);
});
p1.then(msg => console.log(msg));
console.log('end');
Print start
Create the promise and assign it to p1
Run the p1 which prints promise 1 log, then calls setTimeout which invokes the web api. At this point the setTimeout may or may not have already completed. Whenever timer is exhausted, the callback will be put into the macro queue
Print end
Nothing left to execute, so check if the queue has something. The callback from the Macro task is put into the stack and it will run the resolve which puts the callback (the thing after .then) into the Micro task queue.
Nothing left to execute, so check if the queue has something. The callback from the Micro task is put into the stack and it will print promise 1
Related
We know Promises are microtasks and they are added on microtask queue and event listeners/timers are tasks and so they are added on task queue. But what about the event-listeners/timers if they are inside of a promise function(which is passed on the creation of promise to promise constructor). In which queue are they get added?
console.log("script start");
var promise = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("setTimeout2");
resolve();
}, 0);
});
promise.then(function () {
console.log("promise2");
});
setTimeout(function () {
console.log("setTimeout");
}, 0);
console.log("script end");
Here "script start" is the part of main task so it gets executed/printed.
Then the callback function of the promise is handled to browser, the handler adds this to the microtask queue.
the then() pass the callback to the handler of the browser and it will execute after the main promise is resolved.
The setTimeout which is outside of the promise is added to the task queue.
The "script end" gets printed.
Now there are a microtask on the microtask queue and a task on task queue.
So the microtask will be executed first as they have higher priority than the task.
But inside the task, there is a timer.
We know the timers are added to the task queue. But here I think the timer is added to the microtask queue. Because if it was added to task queue then the "setTimeout2" would be printed later because on the task queue there is already a task which prints "setTimeout".
So what actually happens there?
There is nothing special about being "in a promise". The only functions that are put on the microtask queue are the callbacks passed into then.
Here's what's actually happening:
The script start log is executed
The promise is created and calls its executor callback. The executor callback calls setTimeout, which schedules a timer.
The promise is assigned to the promise variable
The then() method is called and attaches a fulfillment callback to the promise
The second setTimeout is called and schedules a second timer
The script end log is executed
This all happened synchronously in one task execution. After some time, the timers fire and (only then!) add their (macro) tasks to the timer event queue. The browser handles the first one:
The setTimeout2 log is executed
The resolve() function is executed which fulfills the promise and adds jobs to run the fulfillment callbacks to the microtask queue
After this macrotask is done, the tasks in the microtask queue are handled:
The promise2 log is executed
Since the microtask queue is empty already, the next macrotask can be handled:
The setTimeout log is executed
I can’t figure out how the code below works, my example of work contradicts the execution of the event loop.
async function main() {
for (let i = 0; i < 10; i++) {
await new Promise(res => setTimeout(res, 1000));
console.log("time + i");
}
}
main();
My example of how this code works:
function main added to Call stack.
call the loop for.
call function Promise which return setTimeout.
setTimeout added to Macrotask.
function main add to Microtask (await resolve Promise).
Some remarks about the steps you listed:
call function Promise which return setTimeout.
Promise is called as a function, but more specifically as a constructor
It doesn't return setTimeout, but ... a promise.
setTimeout added to Macrotask.
setTimeout is not added to a queue. setTimeout is executed, with res and a delay as argument. It returns immediately. The host has registered the callback and timeout using non-JavaScript technology. There is nothing about this in the queues yet.
function main add to Microtask
More precisely, the current execution context of main is saved. Once the awaited promise has resolved, only then will a job be put in the microtask queue with the purpose to resume the suspended execution of main.
Detailed steps
Here are the steps 3, 4 and 5 in more detail:
The Promise constructor is called, which immediately calls the callback given as argument.
setTimeout is called, which registers its callback argument res for later execution.
The setTimeout call immediately returns. Its return value is a unique timer ID, but that value is ignored by the Promise constructor
The Promise constructor returns a promise that is in a pending state.
The await saves the current execution context of main, and makes the main function return with a pending promise.
This is the end of this synchronous execution cycle, and JavaScript now monitors the job queues.
In a following step we have this:
The setTimeout implementation (non-JavaScript) sees that the 1000 delay has passed and places a job in a (macrotask) job queue.
The JavaScript engine gets the job from that queue, and executes it. In this case it means it executes res (starting from an empty call stack).
The call of res will fulfill the promise that was created with new Promise. This creates a job in a (microtaks) job queue to restore the execution context of main.
res returns and the callstack is empty again.
And then, as part of the same "task":
JavaScript reads the microtask queue and executes that job: the main execution context is restored (put on the callstack), with variable i equal to 0 and execution continuing with console.log
The loop makes its second iteration, and the operand of await is evaluated again, calling again the Promise constructor (see the steps at the top). The difference here is that when await makes main return (again) it is returning from a "resume" call that was made from a job, not from JavaScript code. This will be the case for all next executions of await.
And so it continues for all iterations of the (asynchronous) loop. When the loop has completed:
The resumed main function ends with an implicit return undefined. This resolves the promise that main had returned when it had executed await for the first time -- when it returned to the top-level script where main(); had been called.
As there is no code that is awaiting that promise to be resolved, nothing more happens.
Using native Javascript Promise:
Promise.resolve(1).then(it => console.log(it))
console.log(2)
This is logged:
2
1
Question: how is it possible for 2 to execute before 1? JS being event-driven, what is the event that is executing the callback given to then when the original caller has already left that execution tree? Is the engine doing some kind of behind-the-scene magic here?
JavaScript maintains something called a callstack. This is used to keep track of whereabouts in the script we are. When you call a function, it gets added to the callstack, and when the function returns/finishes, it gets removed/popped off the callstack. It is also helpful to think of your entire script also as being in its own "function", and so, when your script first begins its executing, we add "script" to the callstack:
Stack:
- Script
When your Promise resolves, it executes its associated .then() method and adds the callback to something called the micro-task queue. This queue (along with the macro-task queue) is used so that JavaScript can manage asynchronous events. As a result, once you run:
Promise.resolve(1).then(it => console.log(it))
the state of your queues/stacks looks like so (note, this is the state after Promise.resolve(1) and .then() have been added/popped off the callstack):
Stack:
- Script
Micro task queue:
- it => console.log(it)
The callback in the micro-task queue will only execute once it gets added to the Stack. This happens through the use of the event-loop. The event-loop will pull tasks off the micro-task queue only when the callstack is empty. Currently, we are still running "script", so the callstack isn't empty yet.
Next, you encounter your console.log(2), as this is a function call, it gets added to the call stack, and once it returns it gets popped off. This is the reason why you see 2 in the console appear first, as the Promise's .then() callback hasn't executed yet, as it's sitting in the micro-task queue waiting for the main script to finish. One the main script finishes, "script" gets popped off the stack:
Stack:
- (empty) <----------------<
| --- gets moved to the stack by the event-loop
Micro task queue: |
- it => console.log(it) ---^
the event loop then moves the task(s) from the micro-task queue now that the callstack is empty. Once the task is moved to the callstack, it executes, running the second console.log(it). This is why you see 1 logged in the console after the 2.
Run the following code in browser or node.js:
setTimeout(() => {
console.log(1);
Promise.resolve().then(() => {
console.log(2);
});
});
setTimeout(() => {
console.log(3);
});
Promise callbacks and timer callbacks are scheduled differently from each other.
Promise handler functions are called asynchronously at the end of the task that scheduled them. You're scheduling two tasks (the timer callbacks). In the first task, since the promise is already fulfilled, the callback to the fulfillment handler happens asynchronously at the end of the task (it's a so-called microtask). Then the next task (the next timer callback) happens.
So your code executes like this:
The task for executing your script runs:
It schedules the timer callback for the first setTiemout.
It schedules the timer callback for the second setTiemout.
The task ends
The task for the first timer callback runs:
It does console.log(1)
It creates a fulfilled promise
It attaches a fulfillment handler to that promise
Since the promise is already fulfilled, this schedules a microtask to call the handler
The task ends
The microtask queued by the task runs, doing console.log(2)
The task for the second timer callback runs:
And logs console.log(3)
For browsers, you can find the details of this process in the Event Loops section of the specification.
It's worth noting that microtasks scheduled during a microtask run in the same "clear out the microtask queue" step as the microtask that scheduled them, before the next (macro)task runs. Consequently, the following gives us 1 2a 2b 3 rather than 1 2a 3 2b as you might otherwise expect:
setTimeout(() => {
console.log(1);
Promise.resolve()
.then(() => {
console.log("2a");
})
.then(() => {
console.log("2b");
});
}, 0);
setTimeout(() => {
console.log(3);
}, 0);
as does this (because it's largely the same thing):
setTimeout(() => {
console.log(1);
Promise.resolve()
.then(() => {
console.log("2a");
Promise.resolve()
.then(() => {
console.log("2b");
});
});
}, 0);
setTimeout(() => {
console.log(3);
}, 0);
UPDATE WITH DIAGRAMS :
Yes, that's because Promise.resolve() has a different queue called the JOB QUEUE or MICROTASK QUEUE, and this Job Queue has the higher Priority than the Callbacks Queue. Note that we are dealing with Promises now and not the callbacks when we do Promise.Resolve ! So Javascript ES6 came up with this Job Queue to handle Promises differently and call backs differently :)
So, Event Loop is going to check the Job Queue first and make sure there is nothing in that Queue before it starts looking, at the Call back Queue. So that the Job Queue has higher Priority than the call back queue.
I want to give an example to make it more clear because I feel that will make it explain more clearly.
setTimeout(()=>console.log("This is line 1"));
Promise.resolve("Two").then(data=>console.log("I am ",data));
setTimeout(()=>console.log("I am third"));
In the above code snippet, Promise is resolved first and only then the other setTimeout. That's because of the
I was testing out concepts of asynchronous code in JS . Got confused between callback queue & microtask queue order. Whenever promise objects gets resolved , the fulfillment method { then } is pushed into microtask queue while the callbacks of browser timer functions such as setTimeout is pushed into callback queue. Event loop continuously checks queue and pushes functions from queue into call stack whenever call stack gets empty. Event loop should prefer microtask queue over normal callback queue but in the example : https://jsfiddle.net/BHUPENDRA1011/2n89ftmp/ it's happening otherwise.
function display(data) {
console.log('hi back from fetch');
}
function printHello() {
console.log('hello');
}
function blockfor300ms() {
for (let i = 0; i < 300; i++) {
// just delaying logic
}
}
// this sets the printHello function in callback queue { event loop queue }
setTimeout(printHello, 0);
const futureData = fetch('https://api.github.com/users/xiaotian/repos');
// after promise is resolved display function gets added into other queue : Microtask queue { job queue}
futureData.then(display);
// event loop gives prefrence to Microtask queue ( untill its complete)
blockfor300ms();
// which runs first
console.log('Me first !')
expected output
Me first !
hi back from fetch
hello
actual output :
Me first !
hello
hi back from fetch
Kindly let me know how it's happening over here.
Thanks
While it is true, what "kib" stated:
"your function blockfor300ms doesn't block the thread long enough for
the fetch to receive a response"
sadly this is irrelevant, because even if you did block execution until after you received a response to your fetch call, you would still see the same result...
(see sample code snippet below, you can block execution with an alert box or a long loop of non-async XMLHttpRequest calls, I received the same result)
Unfortunately, fetch does not work as described by all the blogs and resources I've found... which state that
Fetch will add it's promise chain to the micro-tasks queue and run before any callbacks on the next tick of the event loop.
From the sample code below, it appears fetch does not simply add it's resolve handler function to the micro-tasks queue as described by others, because as Lewis stated since it requires network activity it is being handled by the Network Task Source. But, I don't believe this is due to printHello "blocking" the network task, because fetch is being fired prior to printHello in my sample code below, and the network response would occur before the timer completed as well.
As you can see in the example below I have the printHello delayed to be added to the Task Queue long after the fetch response has been received (2000ms), yet if we block the code execution for longer than 2000ms (so that there is still running execution context) then "Hello" will be printed first. Meaning the fetch resolve() handler is NOT being simply added to the Micro-Task Queue where it would have fired along with the other promise handlers.
So, why is it still being logged after the callback if the response is received and theoretically added to the Task Queue prior to the timer task completing (at 2000ms)? Well, my guess is that the timer task source must be receiving priority over that of the network task source. And therefore both are sitting in their task queue, but the timer task queue is firing before the network task queue...
Links to specs:
Timer Task Source - https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timer-task-source
Networking Task Source - https://html.spec.whatwg.org/multipage/webappapis.html#networking-task-source
function display(data){console.log("Fetch resolved!")}
function printHello(){console.log("Callback Time")}
function blockExecution() {
console.log("Blocking execution...");
alert('Wait at least 2000ms before closing');
}
const futureData = fetch('https://jsonplaceholder.typicode.com/todos/1');
futureData.then(response => response.json()).then(display);
setTimeout(printHello, 2000);
const p = new Promise(
// this is called the "executor"
(resolve, reject) => {
console.log("I'm making a promise...");
resolve("Promise resolved!");
console.log("Promise is made...");
}
);
p.then(
// this is called the success handler
result => console.log(result)
);
blockExecution();
console.log("Execution ended!");
futureData is actually a fetch promise so there is absolutely a network task queued into task queue when fetch is called. As the result, printHello will definitely be executed before the network task since they're both tasks. And method display will only be put into microtask queue when the promise of network task is resolved. Microtasks, by definition, are only executed at the end of each task. So display will be called at the end of the network task when printHello has already been called long time before.
If you want display to be called before printHello, futureData must only queue microtasks. Let's modify your example a bit.
function display(data) {
console.log('hi back from fetch');
}
function printHello() {
console.log('hello');
}
let now = Date.now();
function executeFutureDataWithMicrotasksOnly() {
// Execute microtasks continually in 300ms.
return Promise.resolve().then(() => Date.now() - now < 300 && executeFutureDataWithMicrotasksOnly());
}
function blockfor300ms() {
for (let i = 0; i < 300; i++) {
// just delaying logic
}
}
// this sets the printHello function in callback queue { event loop queue }
setTimeout(printHello, 0);
const futureData = executeFutureDataWithMicrotasksOnly();
// after promise is resolved display function gets added into other queue : Microtask queue { job queue}
futureData.then(display);
// event loop gives prefrence to Microtask queue ( untill its complete)
blockfor300ms();
// which runs first
console.log('Me first !')
As you can see from the above example, if you replace fetch with a method having only microtasks, the execution order is changed as expected though both fetch and executeFutureDataWithMicrotasksOnly are executed in a similar time interval. When futureData no longer queues tasks, all microtasks including display will be executed at the end of the currently executing task, which is the previous task of task printHello.
I think the problem comes from the fact that your function blockfor300ms doesn't block the thread long enough for the fetch to receive a response.
There won't be anything in the job queue (yet) when the event loop will see that it can call printHello.
Using Visualization
This will help you in giving a better understanding of how javascript works.
Here in this case fetch(Promise) is taking more time than setTimeout so when the event loop cycle is running fetch(Promise) was still in progress and setTimeout is executed first as it took less time and came out of the task queue and it gets processed and when fetch(Promise) ends it comes out of the microtask queue.
If you will increase setTimeout time then the first fetch(Promise) will occur first and then setTimeout will occur. Hope this solves your question.
It seems to be easier than we all think:
It is possible for a microtask to be moved to a regular task queue, if, during its initial execution, it spins the event loop. HTML Living Standard #queue a microtask
When the loop is in the process of selecting a task form the task queue, it can choose to execute tasks that were previously queued into the microtask queue and now are part of task queue:
In that case, the task chosen in the next step was originally a microtask, but it got moved as part of spinning the event loop. HTML Living Standard #event loop processing model
Code that spins the loop is anything that includes parallel operations:
In parallel:
Wait until the condition goal is met.
HTML Living Standard #spin-the-event-loop