I was learning promises in JS and got curious on how promises work with Job queues behind the scenes. To explain my confusion I want to show you this code:
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
return new Promise((resolve, reject) => { // (*)
setTimeout(() => resolve(result * 2), 1000);
});
})
If you look at the above code, is it true that the callback of then() is put into Job queue beforehand and waits for promise to resolve? Or Is it true that callback of then() is pushed into job queue only after promise gets resolved?
When it's time to call a promise callback, the job doesn't go on the standard job queue (ScriptJobs) at all; it goes on the PromiseJobs queue. The PromiseJobs queue is processed until it's empty when each job from the ScriptJobs queue ends. (More in the spec: Jobs and Job Queues.)
I'm not sure what output you were expecting from your code as you didn't say, but let's take a simpler example:
console.log("top");
new Promise(resolve => {
setTimeout(() => {
console.log("timer callback");
}, 0);
resolve();
})
.then(() => {
console.log("then callback 1");
})
.then(() => {
console.log("then callback 2");
});
console.log("bottom");
The output of that, reliably, is:
top
bottom
then callback 1
then callback 2
timer callback
because:
The ScriptJobs job to run that script runs
console.log("top") runs
The promise executor function code runs, which
Schedules a timer job for "right now," which will go on the ScriptJobs queue either immediately or very nearly immediately
Fulfills the promise (which means the promise is resolved before then is called on it) by calling resolve with no argument (which is effectively like calling it with undefined, which not being thenable triggers fulfillment of the promise).
The first then hooks up the first fulfillment handler, queuing a PromiseJobs job because the promise is already fulfilled
The second then hooks up the second fulfillment handler (doesn't queue a job, waits for the promise from the first then)
console.log("bottom") runs
The current ScriptJob job ends
The engine processes the PromiseJobs job that's waiting (the first fulfillment handler)
That outputs "then callback 1" and fulfills the first then's promise (by returning)
That queues another job on the PromiseJobs queue for the callback to the second fulfillment handler
Since the PromiseJobs queue isn't empty, the next PromiseJob is picked up and run
The second fulfillment handler outputs "then callback 2"
PromsieJobs is empty, so the engine picks up the next ScriptJob
That ScriptJob processes the timer callback and outputs "timer callback"
In the HTML spec they use slightly different terminology: "task" (or "macrotask") for ScriptJobs jobs and "microtask" for PromiseJobs jobs (and other similar jobs).
The key point is: All PromiseJobs queued during a ScriptJob are processed when that ScriptJob completes, and that includes any PromiseJobs they queue; only once PromiseJobs is empty is the next ScriptJob run.
I would say callback of then() is pushed into job queue only after promise gets resolved.
If you changed the first timeout to 3000, you run the code and it waits until 3's to alert 1. This is because you have to wait the promise to be resolved in 3 seconds.
You get get the answer from here: https://stackoverflow.com/a/30910084/12733140
promiseA.then()'s callback is a task
promiseA is resolved/rejected: the task will be pushed into microtask queue in current round of event loop.
promiseA is pending: the task will be pushed into microtask queue in the future round of event loop(may be next round)
So here microtask is the same as "job" as you mentioned above, only the promise is resolved or rejected, the callback will be pushed to job/microtask queue.
Related
I've been experimenting with the queueMicrotask() function and I'm not figuring out how callbacks are prioritized when the callbacks are microtasks.
Check out the following code:
function tasksAndMicroTasks() {
const promise = Promise.resolve()
console.log('#1st call')
promise
.then(() => console.log('#3rd call'))
.then(() => console.log('#4th call'))
.then(() => console.log('#5th call'))
queueMicrotask(() => console.log(`I'm microtask from the custom Queue`))
console.log('#2nd call')
}
tasksAndMicroTasks()
then the output is:
#1st call
#2nd call
#3rd call
I'm microtask from the custom Queue
#4th call
#5th call
Then I continue with my experiments and try this:
function tasksAndMicroTasks() {
const promise = Promise.resolve()
console.log('#1st call')
promise
.then(() => console.log('#3rd call'))
promise
.then(() => console.log('#4th call'))
.then(() => console.log('#5th call'))
queueMicrotask(() => console.log(`I'm microtask from the custom Queue`))
console.log('#2nd call')
}
tasksAndMicroTasks()
Then the output is:
#1st call
#2nd call
#3rd call
#4th call
I'm microtask from the custom Queue
#5th call
Therefore my conclusion it is that the "chained promises" are recognized like "second-priority" after the first resolutions and after that the tasks registered using queueMicrotask(callback) API.
Can anyone explain me how the Execution Context Stack handle the chained promises and why the chained ones are differents that the first .then() registration of callback?
A promise returned by .then() is resolved with the result of the callback, so it fulfills only after the callback has been called. Only at this point, the promise handlers of the chained promise are scheduled on the microtask queue - after all the ones that had initially been scheduled on the already fulfilled promise or by queueMicrotask.
Keep in mind that
promise.then(fn1).then(fn2);
is not the same as
promise.then(fn1); promise.then(fn2);
For the purposes of your experiment, Promise.resolve().then(fn) is doing the same thing as queueMicrotask(fn). The Promise is already resolved, so the callback function is queued.
When you chain .then() callbacks, you're adding callbacks to the returned Promises from the calls to .then(). Those Promise objects will not resolve until each .then() resolves in sequence. That's the key difference between your first example and your second. In the second one, you add two .then() callbacks to the same already-resolved Promise, so they're both queued at that point.
Therefore my conclusion it is that the "chained promises" are
recognized like "second-priority"
It's not a difference in priority. If you have the code:
promise.then(a).then(b)
It's not clear until the first promise resolves whether .then(b) will ever be called. The first promise may never resolve.
Therefore .then(b) can't be added to the microtask queue immediately, because it may never need to ever be added to the microtask queue.
I read some files about the microtask and macrotask queue. I know that the callback function of a resolved promise is added to the microtask, same as the other sync tasks. But the following code:
fetch=() => new Promise ((resolve) => resolve ({m: 121}))
function x() {
let d = 3;
fetch().then((data) => {
d += data.m;
console.log("b", d);
});
console.log("a", d);
}
x();
gives a result of
a 3
b 124
IAs I thought, since fetch here fetches a "local" file and it takes no time, so the callback function is immediately added to the microtask queue, and then console.log("a", d); is added to the microtask queue. If so, the result should be
b 124
a 124
So what is the reason for the real result? Why the call back function is executed after console.log("a", d);?
Whether or not a callback is added to the microtask queue or the callback queue has nothing to do with how long it takes to run. The Promise callbacks are always added to the microtask queue. Regardless of how long it takes. And, the queue is cleared, after the call stack is empty. In your example, the function x has to complete execution before your Promise callback can run. So, a is logged first.
Read more about the event loop here. But in a nutshell, browser API callbacks go in the callback queue, and higher priority callbacks like the Promise callbacks go in the microtask queue. The queues are cleared only after the call stack is empty.
This can lead to puzzling behaviors like the following code running forever since the call stack never becomes empty.
let flag = true;
setTimeout(() => flag = false, 200)
while (flag) console.log('hello')
So I been reading a tutorial about Javascript promises these days.
here is an example from it used to explain the macrotask queue(i.e. the event loop) and the microtask queue.
let promise = Promise.reject(new Error("Promise Failed!"));
promise.catch(err => alert('caught'));
// no error, all quiet
window.addEventListener('unhandledrejection', event => alert(event.reason));
It says that because promise.catch catches the error so the last line, the event handler never gets to run. I can understand this. But then he tweaked this example a little bit.
let promise = Promise.reject(new Error("Promise Failed!"));
setTimeout(() => promise.catch(err => alert('caught')));
// Error: Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));
This time he says the event handler is gonna run first and catch the error and after this the promise.catch catches the error eventually.
What I do not understand about the second example is, why did the event handler run before the promise.catch?
My understanding is,
Line one, we first encounter a promise, and we put it on the microtask queue.
Line two, we have a setTimeout, we put it on the macrotask queue,
Line three, we have an event handler, and we put the handler on the macrotask queue waiting to be fired
Then, because microtask has higher priority than macrotask. We run the promise first. After it, we dequeue the first task on macrotask queue, which is the setTimeout. So from my understanding, the error should be caught by the function inside setTimeout.
Please correct me.
You are wrong about step 3). The handler will be added synchronously. Then the microtask queue gets run, and the promise rejects. As no .catch handler was added yet, an unhandled rejection gets thrown.
And I think you are mixing between when a callback gets added and when a callback gets executed. Consider this case:
(new Promise).then(function callback() { });
The callback will be added synchronously, but it will never be called as the promise never resolves. In this case:
Promise.resolve().then(function callback() { });
the callback again gets added synchronously, but the promise resolution happens in a microtask, so the callback will be executed a tick later.
Consider the following code:
function foo() {
console.log('foo');
new Promise(
function(resolve, reject) {
setTimeout(function() {
resolve('RESOLVING');
}, 5000);
}
)
.then(
function(value) {
console.log(value);
}
);
}
foo();
I am trying to understand what happens here correctly:
on executing new Promise the "executer function" is run directly and when setTimeout is called, an operation to add a new entry to the "event queue" is scheduled (for 5 seconds later)
because of the call to then an operation to add to a "job queue" a call to the passed function (which logs to the console) is organised to happen after the Promise is resolved
when the setTimeout callback is executed (on some tick of the event loop), the Promise is resolved and based on point 2, the function argument to the then call is added to a "job queue" and subsequently executed.
Notice I say [a "job queue"] because there is something I am not sure about; which "job queue is it?". The way I understand it, a "job queue" is linked to an entry on the "event queue". So would that be the setTimeout entry in above example?
Assuming no other events are added to the "event queue" before (and after) setTimeout's callback is added, wouldn't the entry for the main code (the call to foo) have been (usually) gone (run to completion) by that time and thus there would be no other entry than setTimeout's for the then's "job queue" entry to be linked to?
on executing new Promise the "executer function" is run directly and when setTimeout is called, an operation to add a new entry to the "event queue" is scheduled (for 5 seconds later)
Yes. More specifically, calling setTimeout schedules a timer with the browser's timer mechanism; roughly five seconds later, the timer mechanism adds a job to the main job queue that will call your callback.
because of the call to then an operation to add to a "job queue" a call to the passed function (which logs to the console) is organised to happen after the Promise is resolved
Right. then (with a single argument) adds a fulfillment handler to the promise (and creates another promise that it returns). When the promise resolves, a job to call the handler is added to the job queue (but it's a different job queue).
when the setTimeout callback is executed (on some tick of the event loop), the Promise is resolved and based on point 2, the function argument to the then call is added to a "job queue" and subsequently executed.
Yes, but it's not the same job queue. :-)
The main job queue is where things like event handlers and timer callbacks and such go. The primary event loop picks up a job from the queue, runs it to completion, and then picks up the next job, etc., idling if there are no jobs to run.
Once a job has been run to completion, another loop gets run, which is responsible for running any pending promise jobs that were scheduled during that main job.
In the JavaScript spec, the main job queue is called ScriptJobs, and the promise callback job queue is PromiseJobs. At the end of a ScriptJob, all PromiseJobs that have been queued are executed, before the next ScriptJob. (In the HTML spec, their names are "task" [or "macrotask"] and "microtask".)
And yes, this does mean that if Job A and Job B are both queued, and then Job A gets picked up and schedules a promise callback, that promise callback is run before Job B is run, even though Job B was queued (in the main queue) first.
Notice I say [a "job queue"] because there is something I am not sure about; which "job queue is it?"
Hopefully I covered that above. Basically:
Initial script execution, event handlers, timers, and requestAnimationFrame callbacks are queued to the ScriptJobs queue (the main one); they're "macrotasks" (or simply "tasks").
Promise callbacks are queued to the PromiseJobs queue, which is processed until empty at the end of a task. Thatis, promise callbacks are "microtasks."
The way I understand it, a "job queue" is linked to an entry on the "event queue".
Those are just different names for the same thing. The JavaScript spec uses the terms "job" and "job queue." The HTML spec uses "tasks" and "task queue" and "event loop". The event loop is what picks up jobs from the ScriptJobs queue.
So would that be the setTimeout entry in above example?
When the timer fires, the job is scheduled in the ScriptJobs queue.
Assuming no other events are added to the "event queue" before (and after) setTimeout's callback is added, wouldn't the entry for the main code (the call to foo) have been (usually) gone (run to completion) by that time and thus there would be no other entry than setTimeout's for the then's "job queue" entry to be linked to?
Basically yes. Let's run it down:
The browser loads the script and adds a job to ScriptJobs to run the script's top-level code.
The event loop picks up that job and runs it:
That code defines foo and calls it.
Within foo, you do the console.log and then create a promise.
The promise executor schedules a timer callback: that adds a timer to the browser's timer list. It doesn't queue a job yet.
then adds a fulfillment handler to the promise.
That job ends.
Roughly five seconds later, the browser adds a job to ScriptJobs to call your timer callback.
The event loop picks up that job and runs it:
The callback resolves the promise, which adds a promise fulfillment handler call to the PromiseJobs queue.
That job ends, but with entries in PromiseJobs, so the JavaScript engine loops through those in order:
It picks up the fulfillment handler callback job and runs it; that fulfillment handler does console.log
That job is completely done now.
More to explore:
Jobs and Job Queues in the JavaScript specification
Event Loops in the HTML5 specification
I'm having trouble understanding why my code below logs the following in order:
"end"
"timeout done"
"promise"
I assumed that "promise" would log before "timeout done" since it has a priority over callback queue task (setTimeout).
My assumption after this observation is that until .then is called, the promise does not enqueue its task since it's not ready yet, and thus allows setTimeout to perform in the callback queue to be performed first. Is this correct?
const sampleFunction = function(e) {
setTimeout(() => console.log('timeout done'), 0)
const data = fetch(`https://jsonplaceholder.typicode.com/comments/1`)
.then(response => {
return response.json();
})
.then(json => {
/*doSomething*/
console.log('promise')
});
console.log('end')
}
Your console.log('promise') has to wait until your fetch().then() is done which involves a networking operation to another host (and thus non-zero time).
Your setTimeout() only has to wait until sampleFunction() returns and you get back to the event queue. So, since the fetch().then() is non-blocking and takes non-zero amount of time, when you first get back to the event queue, only the setTimeout() is ready to go. The networking operation will still be in process in the background.
So, this isn't a matter of prioritization. It's a matter of completion order. The setTimeout() inserts its completion event into the event queue long before the fetch() promise is resolved.
Perhaps you didn't realize that fetch() is non-blocking and calling it just initiates the operation and then your code execution continues after that?
In the console, you should see:
end
timeout done
promise
As long as your fetch() doesn't have any errors.