Understanding async in JavaScript: A Deep Dive into Promises, await, and the Event Loop
A practical deep dive into async, promises, and the event loop in JavaScript — with real-world examples and mental models that stick
JavaScript is single-threaded, but it's also non-blocking.
How? Through asynchronous behavior, powered by the event loop, promises, and microtasks.
If you've ever written async/await and still weren't totally sure why it works (or why it sometimes doesn’t), this post is for you.
We'll go beyond syntax and build a true mental model of how async JavaScript works — and why it matters in every real app.
✅ 1. What async/await Really Means
Let’s start with the basics:
Declaring a function as async changes what it returns.
function regularFunction() {
return 42;
}
async function asyncFunction() {
return 42;
}
console.log(regularFunction()); // 42
console.log(asyncFunction()); // Promise {<fulfilled>: 42}Any function marked with async automatically returns a Promise, even if you return a plain value.
async function run() {
const value = await 42;
console.log(value); // 42
}Yes, you can await a literal value. Why?
Because await will wrap any non-promise in Promise.resolve() behind the scenes.
So this:
const result = await 42;Is equivalent to:
const result = await Promise.resolve(42);Key idea:
awaitwaits for a promise to resolve
If it's not already a promise, JavaScript wraps it as one
This is part of what makes async/await feel so magical — and so easy to misuse.
⚠️ 2. Common Mistakes with async/await
🔹 Forgetting await
const data = fetchData(); // missing 'await' console.log(data); // Promise {<pending>}You just logged a promise, not your data.
If you're inside an async function, always await async calls unless you intentionally want to run something in parallel.
🔹 Mixing .then() and await
const res = await fetch(url).then((r) => r.json());This works, but it's confusing. Pick one style.await was designed to replace .then() for clarity.
Better:
const res = await fetch(url);
const data = await res.json();🔹 Using await in a loop
for (let id of ids) { const user = await fetchUser(id); // runs one-by-one! }This works — but it's slow, because it runs in series.
✅ Better:
const users = await Promise.all(
ids.map((id) => fetchUser(id))
);Now all requests run in parallel — much faster.
🧠 3. Promises, Microtasks & the Event Loop
JavaScript has two async queues:
Macrotasks: setTimeout, setInterval, DOM events
Microtasks: promises,
.then(),await
Try this example:
console.log("start");
setTimeout(() => console.log("setTimeout"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("end");Output:
start
end
promise
setTimeoutWhy?
The
console.log("start")andconsole.log("end")run in the main threadThe promise runs before
setTimeout, because microtasks run first
Rule:
Promises (
await,.then) use the microtask queuesetTimeoutuses the macrotask queue
This explains a lot of “why did that run first?” bugs in UIs or loaders.
🧵 4. async Inside .map()? Not What You Think
This is a classic trap:
const results = [];
[1, 2, 3].map(async (id) => {
const res = await fetchData(id);
results.push(res); // oops
});This does not wait for anything to finish.
.map() doesn't know how to wait for async callbacks — it just returns an array of Promises.
✅ Correct way:
const results = await Promise.all(
[1, 2, 3].map((id) => fetchData(id))
);Now everything runs in parallel, and results is what you expect.
🚫 5. When NOT to Use await
await is convenient — but it can make things slow if used carelessly.
Example:
const a = await fetchA(); // takes 1s
const b = await fetchB(); // takes 1s
const c = await fetchC(); // takes 1sRuns in sequence → takes ~3 seconds.
✅ Better:
const [a, b, c] = await Promise.all([
fetchA(),
fetchB(),
fetchC(),
]);Now all fetches start at once — takes ~1 second total.
Use await sequentially only when needed — when step 2 depends on step 1.
📌 6. Quick Tips You’ll Actually Use
✅ await wraps anything in Promise.resolve()
✅ async always returns a promise
✅ Use Promise.all() to run async code in parallel
✅ Use .catch() or try/catch to handle errors
✅ Never use await in top-level code (unless in a module or REPL)
🧨 7. One-Liner That Breaks Minds
console.log(await (async () => 5)());Yes — you can await inside console.log.
Here’s what happens:
(async () => 5)()returns a Promiseawaitpauses until it resolvesThe value
5is passed intoconsole.log
Weirdly beautiful, right?
🧠 Summary: Your Async Mental Model
await= pause until the promise resolvesasync= always returns a promisePromises = microtasks → faster than
setTimeoutAvoid
.map(async fn)unless you wrap it inPromise.allDon’t make things synchronous unless you really need to
Always know where you are pausing — and why
🔔 Want More Like This?
If you found this breakdown helpful — subscribe.
My goal is to make JavaScript clear without oversimplifying.
You’ll get deep, practical dev posts like this weekly.
Thanks for reading — and feel free to share this with a teammate who’s been console.logging Promises 😉

