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:
- Call stack: where your code runs, one function at a time.
- Web APIs (or Node APIs): where timers, network requests and events wait, outside the stack.
-
Task queue (macrotasks): callbacks from
setTimeout,setInterval, DOM events. -
Microtask queue: callbacks from promises (
.then,await) andqueueMicrotask.
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");
Answer: 1, 4, 3, 2.
-
1and4are synchronous, so they run first. - The promise callback goes to the microtask queue.
- The
setTimeoutcallback goes to the task queue, even with a delay of0. - 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"
And a real asynchronous one:
console.log("Before");
setTimeout(() => {
console.log("Inside the callback");
}, 1000);
console.log("After");
// Before, After, Inside the callback
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
});
});
});
});
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
});
Creating a promise
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
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"));
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.
-
asyncmakes a function always return a promise. -
awaitpauses 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);
}
}
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)]);
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,setTimeoutandPromise.thentogether? - [ ] Can you name the three states of a promise?
- [ ] Do you know the difference between
Promise.allandPromise.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 👇