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 and setInterval are handled, even when passed 0 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 and setInterval, never run immediately. Even if the delay argument is 0. The callbacks are added to the Task Queue.
  • All async code, anything wrapped in a Promise or that comes after await, is added to the Microtask Queue.
  • The tasks in the Microtask Queue will be executed before the event loop moves to the Task 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