Promises and async/await, the short version
A clear, no-magic walkthrough of how promises run on the event loop, where microtasks fit in, and why async/await is just a shorter spelling of the same mechanics.
On this page
JavaScript is single-threaded, yet it almost never freezes while a network call runs. That is the event loop at work, and a promise is just a value that will arrive on it later.
The three states
A promise is in one of three states:
- pending — the work has not finished yet.
- fulfilled — the work finished and produced a value.
- rejected — the work finished and produced an error.
Once a promise settles (fulfilled or rejected), it never changes again. Every .then() or .catch() attached to it, before or after, sees the same outcome.
Microtasks run before anything else
When a promise settles, its callbacks are not invoked immediately. They are placed on the microtask queue, which the event loop drains between every regular task — before setTimeout callbacks, before I/O, before the next frame.
That is why this logs A B C, not A C B:
Promise.resolve().then(() => console.log('B'));
console.log('A');
queueMicrotask(() => console.log('C'));
A runs synchronously, then the microtask queue drains B and C in enqueue order. A setTimeout(0) would land after both — timers are regular tasks.
async/await is a rewrite rule
An async function returns a promise. await pauses the function until the awaited promise settles, then continues on a microtask with its value (or throws its reason).
These two snippets do the same thing:
// with .then
function loadUser(id) {
return fetch('/api/users/' + id)
.then(res => res.json())
.then(user => `${user.name} (${user.email})`);
}
// with async/await
async function loadUser(id) {
const res = await fetch('/api/users/' + id);
const user = await res.json();
return `${user.name} (${user.email})`;
}
The version with await is easier to read, debug (real stack frames), and wrap in try/catch. Reach for it by default.
Run work in parallel, not in series
await creates a sequential dependency. If two calls are independent, start them both first, then await:
// serial — 2× the wall time
const user = await loadUser(id);
const posts = await loadPosts(id);
// parallel — the slower of the two
const [user, posts] = await Promise.all([loadUser(id), loadPosts(id)]);
Promise.all rejects as soon as any input rejects. For "wait for all, keep every outcome," use Promise.allSettled.
Common traps
- Unhandled rejections — a promise with no
.catch()(or notry/catcharoundawait) crashes modern Node and logs loudly in browsers. Always handle the sad path. - Returning inside .then — forget
returnand the next.thenseesundefined.async/awaitsidesteps this. - await in a loop —
for (const x of xs) await work(x)serializes. UsePromise.all(xs.map(work))when the items are independent.
That is the whole model: promises settle once, their callbacks run on the microtask queue, and async/await is a tidier way to compose them.
Written by
More in JavaScript
Two Sum
Given an array of integers and a target, return the indices of the two numbers that add up to it. A classic warm-up for hash maps.
The five array methods you will actually reach for
map, filter, reduce, find and some/every cover almost every array transform you will ever write. Here is when to pick each and the one-line patterns worth memorising.
Discussion
Sign in to ask a question or share what you learned.