JavaScript Promises and async/await: A Practical Guide
Practice3 minInnovialabs editorial team
In short
A promise is an object holding a future result and async/await is cleaner syntax on top of it; run independent requests in parallel with Promise.all and catch errors with try/catch.
The essentials
A promise is an object representing the result of an asynchronous operation: it either fulfills with a value or rejects with an error. async/await is syntax on top of promises that lets you write asynchronous code as if it were synchronous.
Three rules cover most cases:
- catch errors with
try/catcharoundawait; - run independent operations in parallel with
Promise.all; - do not forget
await— otherwise you get a promise instead of data.
From callbacks to promises
Asynchronous code used to rely on callbacks, and nesting grew fast:
getUser(id, (err, user) => {
if (err) return handle(err);
getOrders(user.id, (err, orders) => {
if (err) return handle(err);
render(orders);
});
});
Promises turn this into a flat chain with a single error handler:
getUser(id)
.then((user) => getOrders(user.id))
.then((orders) => render(orders))
.catch(handle);
async/await: same code, easier to read
async function showOrders(id) {
try {
const user = await getUser(id);
const orders = await getOrders(user.id);
render(orders);
} catch (err) {
handle(err);
} finally {
hideLoader();
}
}
Remember: an async function always returns a promise. Even return 5 inside it gives you Promise<5>.
If the server responds with an error status such as 404, fetch does not reject — check response.ok yourself:
const res = await fetch('/api/items');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const items = await res.json();
Parallel or sequential
A common mistake is awaiting independent requests one after another:
// sequential: the second request starts only after the first finishes
const user = await getUser(id);
const news = await getNews();
If the requests do not depend on each other, start them together:
const [user, news] = await Promise.all([getUser(id), getNews()]);
Go sequential only when the next step needs the previous result.
Promise.all, allSettled, race, any
| Method | Settles when | Use it when |
|---|---|---|
Promise.all | all fulfill, or the first rejects | every result is required |
Promise.allSettled | all have settled | you need both successes and failures |
Promise.race | the first one settles, either way | timeouts |
Promise.any | the first one fulfills | several mirrors of one resource |
A timeout with race:
const timeout = (ms) =>
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms));
const data = await Promise.race([fetchData(), timeout(5000)]);
To actually cancel a fetch, use AbortController — race only stops waiting, the request keeps running.
Common mistakes
awaitinsideforEach.forEachdoes not wait for promises. Usefor...offor sequential work orPromise.allwithmapfor parallel work.- A missing
await. The variable holds a promise and the error becomes unhandled. - An empty
catch. A swallowed error is the worst kind of bug. At least log it. - Too many parallel requests.
Promise.allover a thousand items can overload an API. Process in batches. - Mixing styles. Within one function, pick either
thenorawait.
// wrong
items.forEach(async (item) => await save(item));
// parallel
await Promise.all(items.map((item) => save(item)));
// sequential
for (const item of items) {
await save(item);
}