JavaScript Promise Cheatsheet - Async/Await & Chaining

This reference is for JavaScript developers writing async code, whether in the browser with fetch or in Node. It starts from creating and consuming a promise, then static combinators — all vs race vs allSettled vs any, which beginners often mix up — before chaining and the async/await sugar it sits on. Later sections cover real problems: running many requests concurrently without flooding, waiting for the fastest result with a timeout, retrying a flaky request, and aborting an in-flight fetch with AbortController. After reading you should be able to sequence async steps, run parallel tasks with a concurrency cap, and add timeout and retry to brittle calls.

Languages·36 commands·Last updated 2026-07-21
promiseAsyncasyncjavascript

Typical Use Case

For developers converting callbacks to async/await, or who need real control over concurrency and cancellation in Node.js and the browser. The hard part of promises is never syntax but three choices: which of all / allSettled / race / any fits the situation, the order-of-magnitude difference between sequential and concurrent execution, and how to implement timeout, cancellation and retry without leaks. This sheet covers creating and settling promises, the distinct semantics of the four combinators, how async/await maps onto try/catch, concurrency pools and timeouts, plus AbortController cancellation and the deferred pattern.

Basics 5

new Promise((resolve, reject) => { resolve(value) })
Create and resolve immediately
new Promise((resolve, reject) => { reject(new Error("fail")) })
Create and reject immediately
Promise.resolve(value)
Create a resolved Promise directly
Promise.reject(reason)
Create a rejected Promise directly
new Promise((resolve) => setTimeout(resolve, 1000, "ok"))
Resolve after a 1s delay

Consuming Promises 5

promise.then((result) => {})
Register a success callback
promise.catch((error) => {})
Register a failure callback
promise.finally(() => {})
Run regardless of outcome
promise.then(onFulfilled, onRejected)
Specify both success and failure callbacks
promise.then((v) => v + 1)
then return value is auto-wrapped into a new Promise

Static Methods 5

Promise.all([p1, p2])
Succeeds only if all succeed; fails if any fails
Promise.race([p1, p2])
Result of the first to settle (success or fail)
Promise.allSettled([p1, p2])
Wait for all to settle, return each result's status
Promise.any([p1, p2])
First to succeed; fails only if all fail
Promise.allSettled result item = { status, value } or { status, reason }
Result object shape

Chaining 4

fetch(url).then((r) => r.json()).then((data) => {})
Chain to process the response
promise.then(() => nextPromise())
Return a new Promise to sequence async work
promise.then((v) => v + 1).then((v) => console.log(v))
Values pass downstream
promise.catch((err) => fallback()).then((v) => {})
Recover from error then continue the chain

async/await 5

async function fn() { return value }
async function auto-returns a Promise
const result = await promise
Wait for the Promise to settle
try { await promise } catch (err) {}
Catch rejections with try/catch
await Promise.all([p1, p2])
Await multiple tasks concurrently
const [a, b] = await Promise.all([fa(), fb()])
Await concurrently and destructure results

Advanced Patterns 6

for (const item of items) { await process(item) }
Run serially (await one by one)
await Promise.all(items.map((i) => fetch(i)))
Run all tasks concurrently
async function pool(tasks, limit) {}
Limit max concurrency
Promise.race([task(), timeout(5000)])
Timeout control (5s)
async function retry(fn, times = 3) {}
Auto-retry on failure
async function sleep(ms) { return new Promise((r) => setTimeout(r, ms)) }
Delay utility function

Cancellation & Utilities 6

const ac = new AbortController(); ac.abort()
Create and trigger an abort signal
fetch(url, { signal: ac.signal })
Associate the abort signal with fetch
ac.signal.addEventListener("abort", () => {})
Listen for the abort event
AbortSignal.timeout(3000)
Auto-abort after 3s
queueMicrotask(() => {})
Push a callback into the microtask queue
const d = {}; d.promise = new Promise((r, j) => { d.resolve = r; d.reject = j })
Deferred pattern (control resolve externally)

Parameter matrix

ParameterEffectExample
Promise.all([p1, p2])Resolve when all fulfil; reject immediately on the first rejectionconst [a, b] = await Promise.all([fa(), fb()])
Promise.allSettled([p1, p2])Wait for all and report each status; never rejects as a wholePromise.allSettled([p1, p2])
Promise.race([p1, p2])Settle with whichever finishes first, success or failure; the basis of timeoutsPromise.race([task(), timeout(5000)])
Promise.any([p1, p2])Resolve with the first fulfilment; reject only if all rejectPromise.any([p1, p2])
promise.finally(cb)Runs on both settlement paths, ideal for clearing loading state and releasing resourcespromise.finally(() => {})
AbortController / ac.abort()Actively abort the associated asynchronous operationconst ac = new AbortController(); ac.abort()
AbortSignal.timeout(3000)Abort automatically after 3 seconds without manual timingAbortSignal.timeout(3000)
items.map((i) => fetch(i))Start every task concurrently instead of awaiting one by oneawait Promise.all(items.map((i) => fetch(i)))
deferred patternExpose resolve / reject outside the Promise for manual controlconst d = {}; d.promise = new Promise((r, j) => { d.resolve = r; d.reject = j })
queueMicrotask(cb)Queue a callback as a microtask, running before timersqueueMicrotask(() => {})

Common pitfalls

Symptomawait inside a for loop makes ten one-second requests take over ten seconds.

Causeawait in a loop is sequential: each request waits for the previous one, so the total is the sum.

FixGo concurrent: await Promise.all(items.map((i) => fetch(i))); add a pool async function pool(tasks, limit) {} only when you need rate limiting.

SymptomA catch at the end of the chain misses errors thrown earlier, and the page reports Uncaught (in promise).

CauseThe error was thrown in synchronous code that returns no Promise, or the catch was attached to a different non-returning branch.

FixConfirm every link returns a Promise; when mixing callbacks, wrap the whole block in try { await promise } catch (err) {}.

SymptomAfter a Promise.race timeout the request keeps running and the server still logs traffic.

Causerace only settles the outer wrapper early; the underlying task was never cancelled.

FixUse an AbortController: const ac = new AbortController(); ac.abort() together with fetch(url, { signal: ac.signal }); for timeout-only cases AbortSignal.timeout(3000) is enough.

SymptomThe loading state is never cleared on the error path and the UI spins forever.

CauseCleanup lives in the success callback of then and the failure branch has no counterpart.

FixUse promise.finally(() => {}) so it runs on both paths.

SymptomOne failure in a batch discards all the successful results.

CausePromise.all rejects on the first failure, leaving no place to read the other outcomes.

FixSwitch to Promise.allSettled([p1, p2]) and read { status, value } or { status, reason } per entry.

SymptomAn async callback keeps updating state after the component unmounts, with warnings or ghost data.

CauseThe async flow was never bound to a cancellation signal, so the chain continues after teardown.

FixCreate an AbortController in the effect, call ac.abort() on unmount, and use ac.signal.addEventListener("abort", () => {}) to finish up.

Troubleshooting

  1. 1Put an upper bound on how long a promise may take

    Promise.race([task(), timeout(5000)])

    race makes the outer call return after 5 seconds; actually cancelling the underlying task still needs an AbortController.

  2. 2Wrap a callback-style API into an awaitable function

    async function sleep(ms) { return new Promise((r) => setTimeout(r, ms)) }

    This one line converts any callback API into an await-friendly helper and is the most useful glue during migration.

  3. 3Retry automatically on failure

    async function retry(fn, times = 3) {}

    Pair retries with a timeout and backoff, otherwise a network blip hammers the backend.

  4. 4Record an error before it gets swallowed

    promise.catch((err) => fallback()).then((v) => {})

    Returning a fallback value from catch lets the chain continue, which degrades the page gracefully instead of blanking it.

  5. 5Use the deferred pattern when completion is controlled externally

    const d = {}; d.promise = new Promise((r, j) => { d.resolve = r; d.reject = j })

    Useful for bridging callback APIs into async flows, or for deterministic resolve timing in tests.

Command Examples

Minimal shapes: create, fulfil, reject

new Promise((resolve, reject) => { resolve(value) }) new Promise((resolve, reject) => { reject(new Error("fail")) }) Promise.resolve(value) Promise.reject(reason)

Promise.resolve wraps any plain value into an already-settled Promise, and Promise.reject hands you one that is already failed.

Sequential versus concurrent timing

for (const item of items) { await process(item) } await Promise.all(items.map((i) => fetch(i)))

The first awaits one by one and sums up the time; the second creates every Promise synchronously in map so the requests overlap and total time approaches the slowest one.

Choosing between the combinators

await Promise.all([p1, p2]) const settled = await Promise.allSettled([p1, p2]) await Promise.race([p1, p2]) await Promise.any([p1, p2])

all needs every task to succeed, race takes whichever finishes first regardless of outcome, any takes the first success; each allSettled entry is { status, value } or { status, reason }.

Timeout control versus real cancellation

const ac = new AbortController() ac.signal.addEventListener("abort", () => {}) fetch(url, { signal: ac.signal }) Promise.race([task(), timeout(5000)]) AbortSignal.timeout(3000) ac.abort()

race only makes the outer wait settle as a timeout; actually stopping the request requires ac.abort(), while AbortSignal.timeout(3000) raises the abort signal automatically after three seconds.

Common Pitfalls

  • Awaiting inside a for loop stacks the delays; for batch work use await Promise.all(items.map(...)) or a concurrency pool with a limit.
  • Getting a timeout from Promise.race does not stop the underlying request: without ac.abort() it runs to completion and keeps logging on the server.
  • promise.finally is the right place for cleanup such as hiding a spinner; a success-only branch leaves the error path spinning forever.
  • A catch only handles rejections downstream of where it is attached, so a catch hanging off the tail of a chain misses errors thrown in the middle.
  • Promise.all fails as a whole on the first rejection and discards the results of the other concurrent tasks; switch to Promise.allSettled when every outcome matters.

Tips

  • A Promise has three states: pending, fulfilled, rejected - irreversible once settled.
  • then returns a new Promise; chained return values are auto-wrapped and passed to the next then.
  • catch equals then(null, onRejected) and catches rejections anywhere in the chain.
  • Use all when all must succeed, allSettled when you need every result, race for the fastest, any for the first success.
  • async/await is syntactic sugar over Promises; try/catch makes error handling clearer.

FAQ

How do I choose between Promise.all and Promise.allSettled?

The only criterion is whether partial results are acceptable. If later steps need every item, use Promise.all: it rejects on the first failure, which keeps the error path short. If you need every outcome and one failure must not sink the batch, use Promise.allSettled: it waits for all to finish and returns one { status, value } or { status, reason } per task so you decide how to handle failures. With bulk writes, remember that all rejects on the first failure while the remaining tasks are already running in the background and their results will never reach you.

Why is awaiting inside a for loop so slow?

Each await yields the event loop, so the next iteration only starts after the previous one settles: ten one-second requests take about ten seconds. Concurrency means creating every task first and waiting at the end, as in await Promise.all(items.map((i) => fetch(i))). The map builds all Promises synchronously, so the requests are in flight together and the total approaches the slowest one. When you need to cap pressure, use async function pool(tasks, limit) to keep at most limit tasks running.

Does AbortController really cancel the underlying request?

Yes, for operations that honour a signal: pass ac.signal into fetch(url, { signal: ac.signal }) and calling ac.abort() interrupts the request. Note that Promise.race([task(), timeout(5000)]) only makes the outer wait settle first; the inner request keeps running unless you hold the controller and call ac.abort(). Use AbortSignal.timeout(3000) for an automatic three-second abort, and ac.signal.addEventListener("abort", ...) when you need to react the moment it fires.

How does a caller see an error thrown inside an async function?

An async function still returns a Promise, and a throw inside it is equivalent to returning a rejected Promise. The caller either wraps the await in try { await promise } catch (err) {}, or attaches promise.catch((error) => {}) to the chain. The easy mistake is placing the handler where it cannot see the failure: a catch only covers rejections downstream of where it is attached. That is why async/await with try/catch is usually clearer for async work, since the boundaries are visible.

Why does the loading state never get cleared on the error path?

Because the cleanup callback is usually written only inside the success branch. promise.finally(() => {}) runs on both success and failure, so putting the teardown there (hiding the spinner, releasing the connection, removing skeletons) closes the gap. If you also want to inspect each task before closing down, iterate the results of Promise.allSettled([p1, p2]) and branch explicitly instead of returning early in the middle of the chain.

Official References

Each command links to its official documentation below, so you can verify the latest usage and read deeper.

Maintained by LaoHand

Publicly updated on Jul 21, 2026, continuously proofread against official docs.

Contact Us

Wrong command or description? Send us corrections, business inquiries or product feedback by email.

Contact Us