Ace Your Next JavaScript Interview: Promises, Async/Await, Event Loop (Part 4) ✨
Learn the deeper concepts in JavaScript, such as Promises, Async/Await, and Event-Loop. (5 min)
Some time ago, I had an interview, which I almost failed.
The interviewer asked me questions like Promises and Event Loop.
I knew what they were, but I failed to clearly elaborate and explain them.
If you can’t explain it simply, do you understand it really well?
After the interview, I understood that some knowledge was missing.
I decided to dig deeper into JavaScript and fill my understanding and knowledge gaps.
I read many books and articles and took many notes along the way.
Every time I have a JavaScript interview, I review my notes to refresh my knowledge and prepare.
In today’s article, I’ll delve deeper into asynchronous programming in JavaScript, covering Promises, Async/Await, and the Event Loop.
This is part 4 of the “Ace Your Next JavaScript Interview” series.
If you missed the previous articles, see the bottom of this post.
Let’s dive in!
CodeRabbit: Free AI Code Reviews in CLI - Sponsor
CodeRabbit CLI is an AI code review tool that runs directly in your terminal. It provides intelligent code analysis, catches issues early, and integrates seamlessly with AI coding agents like Claude Code, Codex CLI, Cursor CLI, and Gemini to ensure your code is production-ready before it ships.
Enables pre-commit reviews of both staged and unstaged changes, creating a multi-layered review process.
Fits into existing Git workflows. Review uncommitted changes, staged files, specific commits, or entire branches without disrupting your current development process.
Reviews specific files, directories, uncommitted changes, staged changes, or entire commits based on your needs.
Supports programming languages including JavaScript, TypeScript, and more.
Offers free AI code reviews with rate limits so developers can experience senior-level reviews at no cost.
Flags hallucinations, code smells, security issues, and performance problems.
Supports guidelines for other AI generators, AST Grep rules, and path-based instructions.
Promises
A Promise is an object that allows us to take action when a future event happens.
It’s a placeholder for a value that will be available at some point in the future.
Imagine ordering food at a restaurant—you get a receipt (Promise) that represents your order, even though your meal is not ready yet.
Promises have three possible states:
pending - the operation is still in progress
fulfilled - the operation completed successfully
rejected - the operation failed
Upon creation, the Promise takes a function that will be passed two arguments - a resolution callback and a rejection callback.
Most of the time, you’ll be consuming Promises rather than creating them.
The then() method is available on the Promise object and will run with the value passed to the resolve() function.
The catch() method runs if the reject() function was called.
One of the most powerful features of Promises is chaining.
If a then() handler returns another Promise, you can chain additional then() calls.
Async/Await
While Promises are powerful, the syntax can become hard to read and follow with complex chains.
The async/await keywords provide a syntax sugar over the Promises that will make your code read as if it were synchronous.
There are a few key points about async/await:
You can only use await inside functions marked with async.
Async functions always return a Promise.
Use try/catch blocks for error handling instead of .catch().
The code executed asynchronously despite looking synchronous.
Event Loop
Here is where things get really interesting.
JavaScript is single-threaded, meaning it can only do one thing at a time.
So how does it handle asynchronous operations without blocking the entire program?
The answer is its execution flow, which is based on an event loop.
On a high level, the engine runs an endless loop that checks if there are tasks waiting for it in a queue.
If there are, it executes them and continues to wait for more.
Any code that can’t run immediately is queued, and the execution continues until it exhausts the call stack.
setTimeout(() => {
console.log('Later');
}, 1000);
console.log('Now');
// Output:
// Now
// Later (after 1 second)
Even with a zero timeout, the callback still runs after the synchronous code:
setTimeout(() => {
console.log('Later');
}, 0);
console.log('Now');
// Output:
// Now
// Later (after 1 second)
The Event Loop manages two main areas:
Call Stack - where your currently executing code lives.
Task Queue - where asynchronous callbacks wait to be executed.
The Event Loop only processes the task queue when the call stack is empty.
setTimeout(() => {
console.log('Later');
}, 0);
// This loop keeps the call stack busy
for (let i = 0; i < 100000; i++) {
console.log(i);
}
// Output:
// 0
// 1
// 2
// ...
// 99999
// Later
The loop keeps adding items to the call stack and the event loop never gets a chance to reach the task queue.
When asynchronous calls are queued using the same method, for example, setTimeout, their execution order follows the order in which they were added.
setTimeout(() => {
console.log('Later');
}, 0);
setTimeout(() => {
console.log('Even Later');
}, 0)
// Output:
// Later
// Even Later
But if we queue a Promise and a call to setTimeout, the Promise will be the first to run.
setTimeout(() => {
console.log('Later');
}, 0);
Promise.resolve().then(() => {
console.log('Also Later');
});
// Output:
// Also Later
// Later
This is because promises and timeouts are put on two separate queues that have different priorities:
Macrotask Queue (aka task queue) - setTimeout, setInterval, DOM events, etc.
Microtask Queue (aka jobs queue) - Promise callbacks, queueMicrotask(), etc.
The event loop prioritizes the execution of pending microtasks.
Then the event loop will handle the next pending macrotask.
Immediately after every macrotask runs, the event loop will run all pending microtasks before continuing with the macrotasks again.
Here is a nice visualization I found on the Internet:
Let’s see one final code example to check your understanding:
console.log("start");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("Promise.resolve"));
new Promise((resolve, reject) => {
resolve();
console.log("executor");
}).then(() => console.log("promise"));
console.log("synchronous");
To see the answer, see the comments.
📌 TL;DR
Promises give you a clean way to handle future values and chain async operations.
Async/Await is a syntax sugar over Promises, giving you a simple mental model and making your code more readable.
The Event Loop is a fundamental aspect of async programming in JavaScript.
Event Loop handles async operations efficiently in JavaScript.
Hope this was helpful.
See you next time!
Ace Your Next JavaScript Interview Series
👋 Let’s connect
You can find me on LinkedIn, Twitter(X), Bluesky, or Threads.
I share daily practical tips to level up your skills and become a better engineer.
Thank you for being a great supporter, reader, and for your help in growing to 27.7K+ subscribers this week 🙏