Async/Await & Promises
Handle asynchronous operations with Promises and async/await for readable, maintainable control flow.
Promise States and Chaining
A Promise represents a value that may be available now, later, or never. It starts pending, then settles as either fulfilled with a value or rejected with a reason. Once settled, a Promise cannot change state.
Chaining with `.then()` creates a pipeline where each handler receives the previous result. Returning a value from `.then()` wraps it in a resolved Promise; returning a Promise flattens the chain. This composability is the foundation of fetch-based data layers and ORM query builders.
- Three states: pending, fulfilled, rejected
- `.then()` returns a new Promise
- Always return or throw inside handlers
fetch('/api/users')
.then(res => res.json())
.then(users => users.filter(u => u.active))
.catch(err => console.error(err));Async/Await Syntax
The `async` keyword marks a function as returning a Promise. The `await` keyword pauses execution within that function until a Promise settles, then resumes with the resolved value. Code reads top-to-bottom like synchronous logic while remaining non-blocking.
Only use `await` inside `async` functions. At the module top level, ES modules support top-level await when your runtime and bundler allow it, which simplifies initialization scripts.
- `async` functions always return a Promise
- `await` only valid inside `async` functions
- Prefer async/await over long `.then()` chains
async function loadDashboard(userId) {
const user = await fetchUser(userId);
const orders = await fetchOrders(user.id);
return { user, orders };
}Error Handling in Async Code
Wrap awaited calls in try/catch blocks to handle rejections locally. Without try/catch, a rejected Promise from an awaited call propagates to the caller, potentially causing unhandled rejection warnings in Node.js.
For fire-and-forget operations, attach a `.catch()` handler explicitly. Never leave a Promise chain without error handling in production services — unhandled rejections can crash Node processes depending on configuration.
- try/catch around awaited calls
- Re-throw or map errors to domain types
- Always handle fire-and-forget Promises
async function saveRecord(data) {
try {
const result = await db.insert(data);
return { ok: true, result };
} catch (error) {
logger.error('Insert failed', error);
return { ok: false, error: error.message };
}
}Promise Combinators
`Promise.all` runs promises in parallel and rejects if any input rejects — ideal when all results are required. `Promise.allSettled` waits for every promise regardless of outcome, returning status objects useful for batch operations where partial failure is acceptable.
`Promise.race` resolves or rejects with the first settled promise. `Promise.any` resolves with the first fulfillment, ignoring rejections unless all fail. Choose the combinator that matches your failure semantics, not just parallel execution needs.
- `Promise.all` — fail fast, all required
- `Promise.allSettled` — partial failure OK
- `Promise.race` / `Promise.any` — first wins
const [user, settings] = await Promise.all([ fetchUser(id), fetchSettings(id), ]); const results = await Promise.allSettled(tasks);
Production Patterns
Add timeouts to external calls using `Promise.race` with a timer promise to prevent hung requests from blocking resources indefinitely. Implement retry with exponential backoff for transient network failures, but cap retries and use jitter to avoid thundering herds.
In UI code, track loading and error state alongside async operations. AbortController integrates with fetch to cancel in-flight requests when components unmount or users navigate away, preventing stale data from overwriting fresh state.
- Timeout wrappers for external APIs
- Retry with backoff for transient errors
- AbortController for cancellable fetch
const controller = new AbortController();
fetch(url, { signal: controller.signal });
// controller.abort() on cleanup