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.