Updated June 2026. Tested on modern JavaScript (ES2020+). Part of the Techalyst JavaScript series.
JavaScript runs one line at a time, but network calls, timers, and file reads take time. Promises are how the language represents a value that is not ready yet, and async/await is the syntax that makes working with them read like ordinary code. Get comfortable with both and asynchronous JavaScript stops feeling like a tangle.
What a promise is
A promise is an object standing in for a future result. It is always in one of three states: pending (still working), fulfilled (succeeded, with a value), or rejected (failed, with a reason). Once it settles into fulfilled or rejected, it never changes again.
const fetchScore = new Promise((resolve, reject) => {
setTimeout(() => {
const score = 95
score >= 60 ? resolve(score) : reject('Too low')
}, 1000)
})
The function you pass runs immediately and gets two callbacks: resolve for success, reject for failure. Only the first call to either one counts.
Chaining with then, catch and finally
You consume a promise with .then for the value and .catch for errors. The real power is chaining: each .then returns a new promise, so you flatten what used to be nested callbacks into a sequence.
fetch('/api/users/1')
.then((res) => res.json())
.then((user) => fetch(`/api/orders/${user.id}`))
.then((res) => res.json())
.then((orders) => console.log(orders))
.catch((err) => console.error('Something failed:', err))
.finally(() => hideSpinner())
A single .catch at the end handles a rejection from anywhere earlier in the chain, and .finally runs no matter what, which is the natural home for cleanup like hiding a loading spinner. What you return from a .then decides what the next one receives: a plain value passes through, a returned promise is awaited, and a thrown error jumps to .catch.
Coordinating several promises
When you have multiple async operations, four static methods combine them, and picking the right one matters.
// all: every one must succeed, fails fast if any rejects
const [user, orders] = await Promise.all([getUser(), getOrders()])
// allSettled: wait for all, get each outcome, never rejects
const results = await Promise.allSettled([widgetA(), widgetB()])
// race: settle with whichever finishes first (value or error)
const winner = await Promise.race([request, timeout])
// any: resolve with the first success, reject only if all fail
const data = await Promise.any([mirror1(), mirror2(), mirror3()])
| Method | Resolves when | Rejects when | Use for |
|---|---|---|---|
all |
all fulfil | any rejects | need every result, one failure aborts |
allSettled |
all settle | never | dashboards, handle each outcome |
race |
first settles | first rejects | timeouts, fastest wins |
any |
first fulfils | all reject | redundant sources, CDN fallbacks |
race is the classic way to bolt a timeout onto any promise: race it against a promise that rejects after N milliseconds.
async/await: the readable form
async/await is sugar over promises, not a replacement. Marking a function async makes it return a promise and lets you use await inside, which pauses until a promise settles and hands back its value. The same logic reads top to bottom.
async function loadUser(id) {
try {
const res = await fetch(`/api/users/${id}`)
const user = await res.json()
return user
} catch (err) {
console.error('Error loading user:', err)
return null
}
}
Errors are caught with a normal try/catch instead of .catch. This is exactly the cancellable-fetch pattern from the closures post, and the data-loading shape you use inside a Vue watcher or onMounted.
The sequential-await trap
The most common async performance mistake is awaiting independent calls one after another when they could run together.
// slow: each waits for the previous, ~1500ms total
const user = await getUser()
const orders = await getOrders()
const cart = await getCart()
// fast: all start at once, ~as slow as the slowest
const [user, orders, cart] = await Promise.all([getUser(), getOrders(), getCart()])
If the calls do not depend on each other, kick them off together with Promise.all. Only await in sequence when a later call genuinely needs an earlier result.
Wrapping up
A promise is a placeholder for a future value with three states, consumed by .then/.catch/.finally or, more readably, by async/await with try/catch. Reach for Promise.all when you need everything, allSettled when you want each outcome, race for timeouts, and any for redundant sources. And watch the sequential-await trap, run independent work in parallel. Master this and the asynchronous parts of any codebase get a lot calmer.
More in the series: ES6 modules and advanced Axios: cancellation, caching and offline. Questions welcome below.
All comments ()
No comments yet
Be the first to leave a comment on this post.