Menu
CodeSprout
Typo-tolerant search across every article, problem and post. to open

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.

kvi5hnu2 min read
0comments
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:

js
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:

js
// 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:

js
// 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 no try/catch around await) crashes modern Node and logs loudly in browsers. Always handle the sad path.
  • Returning inside .then — forget return and the next .then sees undefined. async/await sidesteps this.
  • await in a loop — for (const x of xs) await work(x) serializes. Use Promise.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

kvi5hnu

More in JavaScript

Discussion

Sign in to ask a question or share what you learned.