← Human InterfacesArchive

Frontend interview prep: asynchronous JavaScript, the event loop, promises and async/await

Oct 28, 20214 min read7 reactions · 0 comments on dev.to

Originally published on dev.to. Comments and reactions live there.

Updated in 2026. I wrote the first version of this post in 2021. I've rewritten it with a corrected callback example, the event loop (the part I was missing back then) and model answers for interviews.

Asynchronous JavaScript is where interviews stop being about definitions and start being about how the language actually runs. Let's build it up from the beginning.

Why JavaScript needs async code

JavaScript runs on a single thread: it executes one piece of code at a time, on one call stack.

That's a problem when something takes time, like a network request, a timer or reading a file. If JavaScript simply waited, the whole page would freeze: no clicks, no scrolling, no animations.

So instead of waiting, JavaScript hands slow work to the environment (the browser or Node.js), keeps running other code, and comes back to the result later. That's asynchronous programming.

The event loop

This is the part that makes everything else make sense, and the part I didn't understand when I first wrote this post.

There are four pieces:

  1. Call stack: where your code runs, one function at a time.
  2. Web APIs (or Node APIs): where timers, network requests and events wait, outside the stack.
  3. Task queue (macrotasks): callbacks from setTimeout, setInterval, DOM events.
  4. Microtask queue: callbacks from promises (.then, await) and queueMicrotask.

The event loop follows one rule: when the call stack is empty, run all microtasks, then one task, and repeat.

The classic interview question

What does this print?

console.log("1");

setTimeout(() => console.log("2"), 0);

Promise.resolve().then(() => console.log("3"));

console.log("4");
Enter fullscreen mode Exit fullscreen mode

Answer: 1, 4, 3, 2.

  • 1 and 4 are synchronous, so they run first.
  • The promise callback goes to the microtask queue.
  • The setTimeout callback goes to the task queue, even with a delay of 0.
  • When the stack is empty, microtasks run first (3), then the next task (2).

If you can explain this step by step, you've answered half of the async questions you'll get.

Callbacks

A callback is a function passed to another function, to be called later.

In my original post, the example was wrong: I passed the result of a function, not the function itself. Here's a real callback:

function greet(name, callback) {
  const message = `Hello ${name}`;
  callback(message);
}

greet("Karen", (message) => console.log(message)); // "Hello Karen"
Enter fullscreen mode Exit fullscreen mode

And a real asynchronous one:

console.log("Before");

setTimeout(() => {
  console.log("Inside the callback");
}, 1000);

console.log("After");
// Before, After, Inside the callback
Enter fullscreen mode Exit fullscreen mode

The problem: callback hell

When async steps depend on each other, callbacks nest:

getUser(id, (user) => {
  getOrders(user.id, (orders) => {
    getInvoice(orders[0].id, (invoice) => {
      sendEmail(invoice, (result) => {
        // ...and error handling at every level
      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Hard to read, hard to handle errors, hard to change. Promises were created to fix that.

Promises

A promise is an object that represents a value that will be available in the future, or an error explaining why it isn't.

It has a state:

State Meaning
pending Still waiting
fulfilled Finished successfully, has a value
rejected Failed, has a reason (usually an Error)

Once a promise is fulfilled or rejected, it's settled, and it never changes again. That's why in this example the reject is ignored:

const promise = new Promise((resolve, reject) => {
  resolve("done");
  reject(new Error("ignored")); // the promise is already settled
});
Enter fullscreen mode Exit fullscreen mode

Creating a promise

function wait(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
Enter fullscreen mode Exit fullscreen mode

Consuming a promise

fetch("/api/user")
  .then((response) => response.json())
  .then((user) => console.log(user.name))
  .catch((error) => console.error("Something failed:", error))
  .finally(() => console.log("Done, success or not"));
Enter fullscreen mode Exit fullscreen mode

Each .then returns a new promise, so you can chain steps instead of nesting them. One .catch at the end handles errors from any step.

Running promises together

This is where interviews often go next:

Method Resolves when Rejects when
Promise.all All succeed Any one fails
Promise.allSettled All finish, success or not Never
Promise.race The first one settles The first one settles with an error
Promise.any The first one succeeds All fail

async/await

async/await is syntax on top of promises that lets async code read like sync code.

  • async makes a function always return a promise.
  • await pauses that function until a promise settles, without blocking the main thread.
async function showUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const user = await response.json();
    console.log(`Hello ${user.name}`);
  } catch (error) {
    console.error("Could not load user:", error);
  }
}
Enter fullscreen mode Exit fullscreen mode

Errors are handled with plain try/catch.

The mistake that shows seniority: sequential vs. parallel

// Sequential: the second request waits for the first. Slow.
const user = await getUser(id);
const settings = await getSettings(id);

// Parallel: both start at once. Faster when they don't depend on each other.
const [user, settings] = await Promise.all([getUser(id), getSettings(id)]);
Enter fullscreen mode Exit fullscreen mode

Pointing this out in an interview, or in code review, shows you think about performance, not just correctness.

Interview answers

"What's the event loop?"
"JavaScript runs on a single thread with one call stack. Async work like timers and network requests is handled by the environment, and their callbacks are queued. When the stack is empty, the event loop runs all pending microtasks, like promise callbacks, and then the next task, like a setTimeout callback. That's why a resolved promise runs before a setTimeout with zero delay."

"Callbacks vs. promises vs. async/await?"
"Callbacks are functions passed to run later; they get hard to manage when nested. Promises represent a future value with a state, can be chained, and centralize error handling. async/await is syntax on top of promises that makes async code read top to bottom, with try/catch for errors."

Quick checklist

  • [ ] Can you explain why JavaScript needs async code if it's single-threaded?
  • [ ] Can you draw the call stack, the task queue and the microtask queue?
  • [ ] Can you predict the output of console.log, setTimeout and Promise.then together?
  • [ ] Can you name the three states of a promise?
  • [ ] Do you know the difference between Promise.all and Promise.allSettled?
  • [ ] Can you turn two sequential awaits into parallel ones?

Previous in the series: functions and closures.

What async bug has cost you the most hours? Tell me in the comments 👇