# How do you review async/await and Promise error handling safely?

> Learn how to review async/await control flow, concurrent Promise.all vs Promise.allSettled behavior, and unawaited floating promises in JavaScript and TypeScript.

- Canonical HTML: [https://unrust.dev/guides/reviewing-async-await-and-promise-chains](https://unrust.dev/guides/reviewing-async-await-and-promise-chains)
- Markdown representation: [https://unrust.dev/guides/reviewing-async-await-and-promise-chains/index.md](https://unrust.dev/guides/reviewing-async-await-and-promise-chains/index.md)

Published: 2026-10-22
Published by Unrust. Scoring and editorial method: https://unrust.dev/help#methodology

## Direct answer

To review async/await and Promise error handling safely, trace what happens when the second of three asynchronous operations rejects: verify that every Promise is either awaited, returned, or explicitly attached to an error handler, check whether Promise.all short-circuits and orphans in-flight sibling side effects, and confirm that cleanup in finally blocks does not mask or overwrite the original rejection.

## Trace partial failure across concurrent Promises

When code runs await Promise.all([chargeCard(), writeLedger(), sendReceipt()]), a rejection in writeLedger() rejects the outer Promise.all immediately—while chargeCard() and sendReceipt() continue running to completion in the background.

During code review, always check whether the tasks inside Promise.all perform non-idempotent side effects that need sequenced ordering or explicit compensation when a sibling task fails.

- [Try the async log timeline explorer](https://unrust.dev/tools/async-log-timeline-explorer/index.md)
- [Read the retry logic review guide](https://unrust.dev/guides/reviewing-retry-logic/index.md)


## Spot floating promises and try/catch return traps

Inside a try { return fetchRemote(id); } catch (err) { ... } block, returning a Promise without return await means a rejection from fetchRemote(id) bypasses the local catch block entirely and rejects at the caller instead.

Similarly, calling an async function inside .forEach(async (item) => ...) discards the returned Promises so the outer function resolves before any async iteration finishes.



## Verify loop concurrency vs sequential ordering

An await inside a for...of loop executes sequentially, preserving order and rate limits, whereas mapping to an array of Promises starts every request concurrently before Promise.all awaits them.

Check whether the downstream API or database lock expects sequential execution or bounded concurrency before approving a refactor between for...of and Promise.all.



## An async/await and Promise review table

Use this table when reviewing asynchronous control flow in JavaScript, TypeScript, or Python asyncio.

### A try/catch block returns the result of an async call directly (return doWork())

- **Review decision:** Check whether the function uses return await doWork() inside try/catch
- **Evidence to verify:** Verify that a rejection from doWork() is caught by the local catch block rather than escaping unhandled to the caller

### An array of items is processed with .forEach(async (item) => ...)

- **Review decision:** Replace .forEach with for...of (sequential) or Promise.all(items.map(...)) (concurrent)
- **Evidence to verify:** Confirm the caller actually awaits completion and surfaces rejections from every item

### Multiple side-effecting operations run together in Promise.all([...])

- **Review decision:** Trace what happens to sibling operations if one Promise rejects mid-flight
- **Evidence to verify:** Verify whether steps must run sequentially or use Promise.allSettled with explicit partial-failure reconciliation

## Try the async review: does the catch block run?

Consider this TypeScript function:
async function loadProfile(id: string) {
  try {
    return fetchUserFromApi(id);
  } catch (err) {
    return { id, fallback: true };
  }
}
If fetchUserFromApi(id) returns a Promise that rejects with a network timeout, what does await loadProfile('u1') do?

- It catches the rejection and returns { id: 'u1', fallback: true }.
- It rejects with the network timeout error because returning an unawaited Promise inside try exits the try block before the Promise rejects.
- It resolves to undefined without running fetchUserFromApi.
- The Promise never settles.

**Reasoning:** In an async function, return promise; resolves/rejects the outer function with that Promise after exiting the try block. Only return await promise; suspends execution inside the try block so a rejection is caught locally.

## Practice loop

1. Check every try/catch in an async function to confirm that returned Promises use return await.
2. Search the diff for .forEach(async or unawaited async helper calls that could create floating Promises.
3. Trace what happens when one branch of a Promise.all call rejects while sibling calls are still in flight.

## Common questions

### Does eslint's no-return-await rule mean I should remove await inside try/catch?

No—inside a try/catch block, return await is required so that a rejection is caught by the catch clause (or before a finally clause runs). Modern TypeScript-ESLint rules specifically require return await inside try/catch.

### Where can I practice tracing interleaved async execution?

Try the free Async Log Timeline Explorer at /tools/async-log-timeline-explorer or take the Unrust diagnostic in JavaScript or TypeScript.

## Limitations

This guide focuses on control-flow and error-handling semantics in async functions; it does not cover OS-level socket tuning or framework-specific request cancellation.

## Provenance

Original editorial guide by the Unrust team referencing ECMAScript Promise semantics and production incident patterns.

## Sources

- [MDN Web Docs: async function and return await in try/catch](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) — Mozilla Developer Network. Documents why return await is required inside try/catch blocks to intercept rejections from a returned Promise.
- [MDN Web Docs: Promise.all() vs Promise.allSettled()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) — Mozilla Developer Network. Specifies fast-fail rejection behavior in Promise.all when any input Promise rejects.

## Related guides

- [How do you review retry logic safely?](https://unrust.dev/guides/reviewing-retry-logic/index.md): Review retry logic by checking retryable failures, idempotency, attempt budgets, backoff, jitter, and the outcome a caller sees when retries stop.
- [How do you trace root causes in interleaved production logs?](https://unrust.dev/guides/reading-production-incident-logs/index.md): Learn how to separate the first loud ERROR symptom from the earlier root-cause event when reading interleaved async production logs.
- [What is code tracing?](https://unrust.dev/guides/code-tracing/index.md): Learn code tracing with a worked JavaScript loop, step-by-step state tables, an array-alias example, and a free reasoning check.

Next step: [Take the free diagnostic](https://unrust.dev/diagnostic/index.md)
