Technical Interview Questions I Wish I Could Do Over #1
JavaScript Execution Model
Technical interviews are the adult equivalent of those big test days we had in grade school that would determine our academic future. They’re challenging to prepare for because sometimes I know ahead of time what the topic will be (system design lite) and other times it’s a surprise I learn once I’m in the interview (build an infinite scroll in react), and there’s almost always something I wish I could do over after I finish.
This series is intended to be part therapeutic, part knowledge share: a place to break down questions I wish I could do over. Whether you're prepping for interviews, brushing up on fundamentals, or just enjoy a bit of technical storytelling, I hope these posts are useful and maybe even a little cathartic. Who knows, maybe I'll write about technical interviews I do well in too.
What will be the output of the following code and why?
console.log('start');
setTimeout(function setTimeoutFunc() {
console.log('timeout');
}, 0);
Promise.resolve().then(function firstPromiseFunc() {
console.log('promise 1');
});
async function asyncFunc() {
console.log('async start');
await Promise.resolve();
console.log('async end');
}
asyncFunc();
console.log('end');
After taking a minute to read through the code this was my response:
start, timeout, promise 1, async start, end, async end
The actual output is:
start, async start, end, promise 1, async end, timeout
I misunderstood two big parts of the execution model:
- how
setTimeout
andsetInterval
are handled, even when passed0
to their delay argument - the microtask queue
There are three key parts to the JavaScript execution model: the Call Stack
,
the Microtask Queue
, and the Task Queue
.
| Call Stack | Microtask Queue | Task Queue |
When the program runs the first line it evaluates is:
console.log('start');
This operation is added to the Call Stack
| Call Stack | Microtask Queue | Task Queue |
| --- | --- | --- |
|console.log('start')|
Since this operation is synchronous it is executed immediately and removed from
the stack. So we see start
logged and the Call Stack
returns to its previous
state.
| Call Stack | Microtask Queue | Task Queue |
| --- | --- | --- |
Pretty simple so far.
The next line evaluated is the setTimeout
. Here I assumed the setTimeout
would be added to the Call Stack
, the timer started, and since it was 0
, the
callback function would be executed immediately. But what really happens is the
callback function (setTimeoutFunc()
) is added to the Task Queue
and the
program moves to the next line to evaluate.
| Call Stack | Microtask Queue | Task Queue |
| --- | --- | --- |
| | | setTimeoutFunc()|
The next block of code that is evaluated is:
Promise.resolve().then(function firstPromiseFunc() {
console.log('promise 1');
});
Since this is an asynchronous function it is added to the Microtask Queue
| Call Stack | Microtask Queue | Task Queue |
| --- | --- | --- |
| |firstPromisFunc()| setTimeoutFunc()|
And the program moves to the next lines:
async function asyncFunc() {
console.log('async start');
await Promise.resolve();
console.log('async end');
}
asyncFunc();
The asyncFunc()
is added to the call stack:
| Call Stack | Microtask Queue | Task Queue |
| --- | --- | --- |
| asyncFunc() |firstPromisFunc()| setTimeoutFunc()|
The first console.log
statement is synchronous so it is executed logging
async start
and then the program gets to the await
keyword. await
is
syntactic sugar for native Promises so the event loop treats this the same way
it did the previous Promise
, the console.log('async end')
is scheduled in
the Microtask Queue, and at that point, asyncFunc()
is paused and removed from
the call stack. Its remaining execution (everything after the await) will resume
in the microtask phase of the event loop.
| Call Stack | Microtask Queue | Task Queue |
| --- | --- | --- |
| |firstPromisFunc() | setTimeoutFunc()|
| |console.log('async end')| | |
The program then moves on to the last line:
console.log('end');
and since this is a synchronous operation it is added to the stack and executed immediately.
The end of the program has been reached so the event loop moves to the
Microtask Queue
. The Task Queue
and the Microtask Queue
both operate on a
First in, First out basis. For my example, firstPromiseFunc()
was added to the
queue first so it is executed first and promise 1
is logged. Then
firstPromiseFunc()
is removed from the queue. The event loop checks to see if
there are any tasks left in the Microtask Queue
and since there is it is
executed next and we see async end
logged.
Now that the Microtask Queue
is empty the event loop moves to the Task Queue
and executes tasks on the same First in, First out basis. For this example we
now see timeout
logged.
Key Takeaways
- Timer functions,
setTimeout
andsetInterval
, never run immediately. Even if the delay argument is0
. The callbacks are added to theTask Queue
. - All async code, anything wrapped in a
Promise
or that comes afterawait
, is added to theMicrotask Queue
. - The tasks in the
Microtask Queue
will be executed before the event loop moves to theTask Queue
.
This question reminded me how important it is to understand not just what JavaScript does, but when it does it. Before this technical interview I would've said I had a good mental model of how async code works but realizing how the microtask and task queues work helped me connect a few lingering gaps. Now that I’ve internalized the way the event loop processes Promises, async/await, and setTimeout/setInterval, I’ll be more confident when questions like this come up.
Next up: a question I actually nailed and how I approached it.
Further Reading
- MDN JavaScript execution model
- MDN In Depth: Microtasks and the JavaScript runtime environment
- JavaScript Visualizer 9000
(There seems to be an issue with the
await
keyword in the JavaScript) visualizer so I've rewritten that code usingPromise
. I've also added alog
function to help with the visualization)