Promises and async/await
Asynchronous work, error handling, and the model behind data fetching.
What you'll learn
- What a promise represents, and its three states
- Reading
.thenchains, and writing the same thing withasync/await - Handling errors in both styles
- Running work in parallel instead of one after another
The problem
JavaScript runs on a single thread. If it waited for a network request, the page would freeze. So slow work is started now and finishes later, and you describe what should happen when it does.
A promise is the object representing that eventual result. It is in one of three states: pending, fulfilled with a value, or rejected with an error.
Note the ordering below — the last line runs before the promise resolves:
then and catch
.then registers what to do with the value. Returning from a .then passes the
result to the next one, which is what makes chains work. .catch handles a
rejection anywhere earlier in the chain.
async and await
async/await is the same machinery with flatter syntax. An async function
always returns a promise, and await pauses inside it until a promise settles.
Note that run() still returns a promise — await only pauses inside the
async function, never the program around it.
Errors with try/catch
Because await unwraps the promise, a rejection surfaces as a thrown error, and
ordinary try/catch works:
An unhandled rejection is a real bug — always have a catch, in either style.
Sequential vs parallel
This is the mistake worth internalising. Two awaits in a row run one after
the other. If the tasks do not depend on each other, that is wasted time.
Start them both, then await both — Promise.all takes an array of promises and
resolves with an array of results:
Roughly 800ms against 400ms, for the same work.
Promise.all rejects as soon as any one of its promises rejects. When you want
every result regardless, Promise.allSettled gives you a status for each.
Fetching data
Real code mostly awaits fetch. The shape below is the one you will reuse in
Fetching Data once components are involved:
async function loadUser(id) {
const response = await fetch("/api/users/" + id);
// fetch only rejects on network failure, not on 404 or 500.
if (!response.ok) {
throw new Error("Request failed with " + response.status);
}
return response.json(); // also a promise
}
That response.ok check is easy to forget. A 404 is a successful HTTP
exchange as far as fetch is concerned.
Common mistakes
- Forgetting
await. You get a pending promise instead of a value, and it usually shows up asundefinedsomewhere later. - Awaiting in sequence for no reason. Use
Promise.allfor independent work. - Assuming
fetchthrows on 404. Checkresponse.okyourself. - No rejection handling. Every chain needs a
catch, or the async call needs atry.
Exercise
loadAll fetches three things one after another and takes about 900ms. Make it
run them in parallel so it takes about 300ms, keeping the same output order.