Promises and async/await
From callbacks to Promises, and finally to async/await — the modern way to write async code that reads top-to-bottom.
The previous chapter taught you the idea of asynchronous code using plain callbacks. Modern JavaScript wraps that idea in two much nicer tools:
- Promises — objects that represent a future value.
async/await— syntax that lets you write Promise-based code as if it were synchronous.
By the end of this chapter, you'll be able to read and write real-world async code.
A Promise is a placeholder
When you start a slow operation, a Promise is the thing you immediately get back. It's a placeholder for a value that isn't ready yet. Later, the Promise will either be:
- fulfilled with a value, or
- rejected with an error.
Once a Promise is settled (fulfilled or rejected), it never changes.
Creating and consuming a Promise
You usually consume promises returned by other functions, but occasionally you create one yourself.
new Promise((resolve, reject) => { ... }) calls your function
immediately, passing in two functions. You call resolve(value)
when the work succeeds, or reject(error) if it fails.
You consume a promise with .then() (for success), .catch()
(for failure), and .finally() (always).
Chaining replaces nesting
Remember callback hell? Here it is with Promises instead.
No more rightward drift. Each .then returns a new promise, so
chains compose naturally. A single .catch at the end catches
any error in any step.
A vital rule: return the next promise from inside .then. If
you don't, the chain has no idea to wait for it.
async/await: promise-based code that reads top-to-bottom
async/await is just syntactic sugar over Promises — but it's
the syntax most modern code uses.
- A function marked
asyncalways returns a Promise. - Inside an
asyncfunction,await somePromisepauses the function until that promise settles, then evaluates to its fulfilled value (or throws its rejection).
That code does the same thing as the previous example, but it
reads like a recipe. This is why async/await quickly became
the default style.
Error handling with try/catch
await throws on rejected promises, so you can use plain
try/catch.
Try, catch, finally — the same shapes you already know from
synchronous code. That's the magic of async/await: async code
that looks like the synchronous code you've been writing all
along.
Running things in parallel: Promise.all
await waits for one thing at a time. Sometimes you want to
start several operations at once and wait for all of them.
sequential takes ~900ms; inParallel takes ~300ms. The
difference is when you await: starting all the promises before
the first await lets the host work on them concurrently.
A common bug: writing the parallel version like this —
const a = await fakeFetch("/a", 300);
const b = await fakeFetch("/b", 300);— which is sequential, because each await blocks before the
next call starts.
Promise.allSettled, Promise.race, Promise.any
A quick tour of the other combinators:
When you need to call several independent APIs, Promise.all (or
allSettled if you want to keep going past errors) is almost
always what you want.
Common pitfalls
A short list of "gotchas" that catch beginners.
Forgetting to await
If a variable holding "the value" turns out to be a Promise, you
forgot an await.
Using await inside a forEach
forEach ignores the promise that an async arrow returns, so
your code doesn't wait.
The rule: when you need to await inside iteration, prefer a
plain for...of loop (sequential) or Promise.all(arr.map(...))
(parallel).
Unhandled rejections
A rejected promise that has no .catch (and isn't awaited in a
try/catch) becomes an unhandled rejection. Always handle
errors somewhere — at the top of your chain at minimum.
Mental model: await is a "pause point"
When the engine hits await expression, it:
- Evaluates
expression. If it isn't a Promise, wraps it inPromise.resolve(...). - Suspends the async function.
- Attaches the rest of the function as a microtask reaction.
- Returns to whatever called the async function.
- Later, when the promise settles, the microtask runs and the
function continues at the line after
await.
Holding this picture in mind makes the printed-output puzzles predictable.
Multi-file: a tiny client
Challenge
Write an async function retry(fn, attempts) that calls fn() (which returns a Promise).
- If it fulfils, return its value.
- If it rejects, try again, up to
attemptstotal times. - If all attempts fail, reject with the last error.
- Assume
attemptsis a positive integer.
Hints:
- Use a
forloop withtry/catch. - Keep track of the last error so you can throw it at the end.
Which snippet runs the two fetches concurrently (overlapping in time)?
Hint: look for the version where both fetch calls are started before any await pauses execution.