Check out a free preview of the full Test Your JavaScript Knowledge course

The "Event Loop & Task Queue" Lesson is part of the full, Test Your JavaScript Knowledge course featured in this preview video. Here's what you'd learn in this lesson:

Lydia explains the concept of the event loop in JavaScript. They discuss the different components of the event loop, such as the call stack, task queue, and microtask queue. They also provide examples and explanations of how different tasks and callbacks are scheduled and executed in the event loop.

Preview
Close

Transcript from the "Event Loop & Task Queue" Lesson

[00:00:00]
>> Let's just get started. The first category is the event loop, just to make sure we're all on the same page. The event loop, so we have a very simple script, we're just console logging 1 and then we have a function, myFunction that we then invoke. So first console log(1) gets added to the call stack.

[00:00:14]
We log(1) and returns a value, we're done. Then we're invoking myFunction, myFunction gets added to the call stack, this in turn, invokes console log(2), 2 gets added or gets logged to the console, and then got popped off the call stack. This is very straightforward we kind of know how it works.

[00:00:32]
We'll talk more about the other components of the event loop in just a moment. But then we also have setTimeout, and setTimeout works a bit different because this is a web API that we can use to schedule the callback that we pass to the macro-test queue after a certain delay.

[00:00:48]
And this is an important thing to remember, cuz it's not that we're delaying when it gets pushed to the call stack, it's when it gets pushed to the task queue. Again, we'll kind of see how this works in just a moment. So in this case, we have the setTimeout, and we have a callback function that is just logging 1 with a zero millisecond delay.

[00:01:06]
And this is kind of a tricky one, cuz you're like, it's zero milliseconds, would it just run it right away, that's not really the case. So setTimeout gets pushed on the call stack. And this in turn, pushes that callback to the web API's like timer API. So we have a very, well, zero millisecond delay, but it's still kind of on the next step.

[00:01:25]
So this pushes it to the microtask queue or just the task queue, whatever you wanna call it. And then on the next iteration or the next loop, it'll actually push it to the call stack, which then logs(1). So you can see now that if there were other functions in that script, it wouldn't have blocked the call stack.

[00:01:41]
So if this was something that might have run later, we can just keep running our script without blocking any of that or the execution. Then we also have Microtasks, and this is a bit different because when we invoke queueMicrotasks or any other parts of the code that would create a microtask and just pushes it to the microtask queue immediately.

[00:02:04]
And then again, once the call stack is empty, it pushes it to the call stack and then logs(1). One important thing to remember about microtasks is that they get executed before the tasks in the regular task queue. So in this case, it just keeps checking, all right, are there any microtasks to process instead of the task that's queued there?

[00:02:24]
Yes there are, so then only afterwards will it actually process the tasks. So these are all microtasks, and there aren't any, or these are the most common ones. So first, we have the asyncFunction, and the code after the await keyword is put it in a microtask or like process as a microtask.

[00:02:46]
And this is something important to remember that once we have the await keyword the rest of the body will be executed as a microtask. Same for the then callback catch and finally, callback functions. Of course also Q microtasks, mutation observer, and the process next tick. So just remember that these are all microtasks, not regular tasks.

[00:03:06]
So then we have the very first question to put the logs in the correct order. So we have the promise resolve, the then callback, queueMicrotasks, setTimeout, a regular console.log, a new Promise constructor, and then the asyncFunction. So it's up to you to put these in the correct order, how you think that they'll be logged, and then afterward, we'll see if you have it right.

[00:03:30]
All right, so the correct order would be 4, 5, 6, 1, 2, 3. So first, let's just see what happens when we execute the script. So the very first thing is that we call promise.resolve. And this just gets added to the call stack, cuz it's a regular invocation and it's an immediately resolved promise.

[00:03:49]
So the then callback is, or the then function, it's pushed to the call stack right away, which then schedules that microtask, cuz it's the callback to the then chain method, as we just saw, that is a microtask. Then we call queueMicrotask. So that queueMicrotask is added to the call stack, but that then schedules that microtask, which is console.log(2).

[00:04:11]
Then we have the setTimeout, gets added to the call stack, and that schedules the callback and the Web API. Which on the next tick gets pushed to the regular task queue, and at the same time, console.log(4) is pushed to the call stack. This is just a regular function, so four is logged as the very first value.

[00:04:29]
Then on the next line, we have the new Promise constructor, and the body of new Promise is just executed synchronously, and that can be a bit counterintuitive. Cuz a lot of people think that the new Promise constructor body is also run asynchronously, that's not the case. It's only like that the result will execute the rest asynchronously.

[00:04:49]
We have this is just pushed to the call stack, which then log(5). So the second log is five. And then we have the immediately invoked asyncFunction. And again, also here, that body is just run synchronously. It's only the awaited value where the asynchronous part kind of begins, we'll also look at that in just a second, but this is just the first thing.

[00:05:12]
So console.log(6) and 6 gets logged to the console. So now, the call stack is empty. So the first thing to happens is that the microtask queue gets checked and the first task there gets added to the call stack. So the callback for the then method, so call or console.log(1), 1 gets logged and the same for 2, which was that queue microtask callback.

[00:05:36]
So the next one is 2. And then there's nothing in the microtask queue. There was something in the task queue, so the thing is pushed to the call stack and logged logs 3. So that's kind of this whole event loop order, I guess. Question number 2, which of the following are not considered microtasks in the JavaScript's event loop?

[00:05:58]
Is it script loading, setTimeout callback, mousemove events, requestAnimationFrame callback, UI rendering, the then callback, the new Promise executor function, or fetch calls? So there can be multiple or all of them. The answer is pretty much all of them. It's just a then callback that is considered a microtask, cuz we just saw here at that it's just the asyncFunction after the await keyword when the promise actually resolved or the rest of the asyncFunction body is a microtask.

[00:06:30]
Or the callback pester, then catch finally, Q microtask and mutation observer, or next tick, but everything else is just a regular task. Ready for next question? Again, putting the logs in the correct order, but this time, we have an asyncFunction. In which, in the body, we log 1 then we have a new Promise constructor logs (2) or waiting a new Promise with a set timeout in it and then resolve.

[00:06:54]
And then we have another new Promise constructor that logs 4 and immediately called asyncFunction in there, where we're awaiting the asyncFunction that we just had above that. We have a then change method. And then we're just logging eight in the normal script. So in what order would they get logged [LAUGH]?

[00:07:15]
Please never write JavaScript like this, by the way, this is the worst. And the right order is 4, 5, 1, 2, 8, 7, 6, 3. So let's see why. First, we just have a regular function declaration so nothing runs yet. Then the very first thing is that new Promise constructor.

[00:07:39]
So that gets pushed to the call stack, and then the very first thing in its body is the console.log(4). So, the first thing that gets logged is 4. It gets popped off the call stack, and then we have the immediately invoked asyncFunction. So this body runs right away, so console.log(5), so 5 is the next one.

[00:07:56]
And then we await asyncFunction, but that means the asyncFunction is still executed pretty much synchronously. So this body still runs right away. So the very first thing in this asyncFunction body is console.log(1). So the next one that gets logged is 1. Then we have the new Promise constructor, and again, the new promise constructor's executor function is executed right away.

[00:08:19]
So this log(2). Then we await new Promise, but that means that we're still executing that awaited promise, so we can have that new Promise constructor. And the very first thing on this line is you set timeout. So this callback's added to the timer API, and then on the next tick, it all gets pushed to the task queue.

[00:08:39]
And then we resolve that promise. Now, one thing about asyncFunctions is that, after that awaited value has resolved, that's when the rest of the asyncFunction is pushed or put in that micro task that we just saw. But you probably know that if we don't return or explicitly return a value from a function, it implicitly returns undefined.

[00:08:58]
So, even though we're not returning anything, that asyncFunction is still an output in the microtask queue. But it's just the rest that we still had to execute, even though that's the implicit return undefined. But that also means that we're awaiting asyncFunction here, but it hasn't resolved yet, it's still pending.

[00:09:15]
So the async body here, or asyncFunction, is then paused, cuz there's nothing, like the promise hasn't resolved yet. So that's just something important to remember with asyncFunction is that once the awaited value is still pending, that entire function is paused. And JavaScript itself will then return to the outer scope, which in this case is resolve for the new Promise constructor.

[00:09:38]
So this promise resolves now, which means that the then callback is executed. So then gets pushed to the call stack and its callback function is then pushed to the microtask queue. And then we return back to that outer scope, so console.log(4), which is just a regular log, so this log(8).

[00:09:56]
Now, there's nothing else on the call stack. So the very first thing on microtask queue is that asyncFunction body, which doesn't do anything cuz it was that implicit undefined or implicit return undefined. But it does mean that in our result, cuz so, asyncFunction is now a resolved promise.

[00:10:12]
So you can see that the rest of that, like immediately invoked asyncFunction and the new Promise constructor now gets pushed to the microtasks queue. Cuz it means that the rest of that function body, so the console.log(6), will get logged eventually, but it's just that body. So now the first thing on the microtask queue is the console.log(7), so this log 7.

[00:10:33]
And now the rest of the asyncFunction body here is executed, so that is that console.log(6), so that logs 6 [LAUGH]. And then finally, this all gets popped off, and then we have the regular task queue that was for that setTimeout, which is console.log(3), and then 3 gets locked at its very last value.

[00:10:51]
Does that make any sense? Is it kinda clear how that works? Maybe I can also just show you, I think it was this one. I mean, there isn't much, I just wanted to show you that it's actually that value that gets logged, let's see, node 3. Yeah, so what I was talking about here is, I actually always say return undefined, but it means that here, once this promise has resolved, all of this gets a few dozen microtasks here.

[00:11:21]
Same with this, so this is why it logged 6 eventually. But it was a bit later, cuz it was a microtask, but still before that set timeout because that was the only regular task that got queued. Hope that makes some sense at least.
>> So normal invocations then promise, then async, microtask, and then macrotask.

[00:11:48]
>> Well, not necessarily promise, yeah, the promise executor function or executor function, I never know how to say that, that is just run like a regular function. And the asyncFunction is also synchronous until it hits that await keyword and the rest after is the asynchronous part. Because of course, asynchronous functions are just kind of like synthetic sugar over a new Promise and a then callback.

[00:12:11]
So the fact that that then callback was a microtask is essentially the same as saying everything after the await keyword is a microtask, it's just replicating that same behavior. Cuz all you want is if we're awaiting a value, we wanna make sure that we actually have access to that value after the wait keyword, which is essentially the same as saying .then and then res, and then doing something with res.

[00:12:34]
But I think the way that, that was first phrased was not necessarily correct cuz it's not always the case. Does that make sense? It's not then a function, then a promise, depends on the promise and if it resolves right away, or rejects, or is always pending.
>> So setTimeout won't always be last?

[00:12:55]
>> Well, tasks on the regular task queue, they will always be after a microtask. And also, microtasks can schedule other microtasks, which means that, if you just have a microtask that's always returning another microtask, that regular task will never stand a chance, that will never get added to the call stack.

[00:13:15]
But, yes, anything that gets added to that task queue will always be executed last in the event loop. Unless that task then schedules another microtask, cuz then it's like task micro task, task. Yeah, I feel like I'm just making it so much worse now, but something important to remember, so microtask and schedule another microtask.

[00:13:41]
And then they will still get all invoked before the first task is added to the call stack. All right, so this is gonna be the last question kind of about the event loop like this. So after this you're you don't have to worry about it anymore, but choose the correct logs once again.

[00:13:56]
So we have an immediately invoked asyncFunction. And within an asyncFunction we have an arrow asyncFunction that just returns the string asyncFunc. We have a promise, a variable that is the new Promise constructor logging promise. Notice, here how we don't result from that promise by the way, just yell.

[00:14:16]
We have a then call back that logs than a regular log that logs async body, queue microtask, and the results, that's the promise to all. Just know the promise to all method is you can use that to run multiple promises at the same time, and then it'll give a result once they've all resolved.

[00:14:34]
And then we have the console.log script that runs after that asyncFunction is finished and running. So, what would be the correct logs? Would it be promise async body, script queueMicrotask, promise Async body script, then queueMicrotask? Script, promise, async body, then queueMicrotasks or, yeah, honestly, I feel like if I say all the options out loud it's It's just gonna be work of using.

[00:15:01]
Yeah, or D or E. So the correct answer here is A, first, promise, than async body, scripts, queueMicrotask. So first, the asyncFunction is invoked right away. So that's, first gets added to the call stack. Then we have the new Promise constructor, also gets added to the call stack, and its body runs synchronously.

[00:15:26]
So console.log promise gets added to the call stack, and this logs promise. Note here that we don't resolve, which means that the then method or the then callback never gets called, cuz that only gets called when we actually resolve from the promise. So this just gets ignored, that part.

[00:15:42]
Then the next part is console.log async body, so async body gets logged. Then there's a queueMicrotask, gets added to the call stack and queues that microtask. And then we have the promise.all. We already have that promise that's still pending cuz we never resolved, but asyncFunction is called and this just returns the string so nothing gets logged.

[00:16:01]
It's just returning, it's resolving with that string as its value by not logging anything, so we don't really care. Then here we have console.log, sorry, there was something wrong with the animation there, the queueMicrotask trips to be in their previous one as well. But then, we have console.log(script), so it gets pushed to the call stack script.

[00:16:22]
And then, the call stack is empty, so queueMicrotask gets added, and that gets logged. And then we finish the script, make sense? So this would have been different if we returned a value here, or a resolve here, cuz then, the then would've been called, and then would've been added to the microtask queue.

[00:16:43]
But we didn't, so this never runs. Yeah, just something to remember.
>> Can a race condition happen here?
>> With Promise.all? I believe so, I mean, it depends on when they resolve. Yeah, I mean, that's not really in the scope of this specific question, cuz one of them is always pending, but yeah, with Promise.all that's known to happen as far as I know.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now