async/await in Node
async/await makes asynchronous Node code read like synchronous code while keeping Promise semantics - the win is clarity if you preserve error context and avoid serializing independent I/O.
Search across all documentation pages
async/await makes asynchronous Node code read like synchronous code while keeping Promise semantics - the win is clarity if you preserve error context and avoid serializing independent I/O.
async function fetchUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<{ id: string; name: string }>;
}
try {
const user = await fetchUser('42');
console.log(user.name);
} catch (err) {
console.error('fetchUser failed:', err);
}When to reach for this:
.then() pyramids in brownfield codeimport { createServer } from 'node:http';
interface User { id: string; name: string }
const users = new Map<string, User>([['1', { id: '1', name: 'Ada' }]]);
async function getUser(id: string): Promise<User> {
await new Promise((r) => setTimeout(r, 10)); // simulate DB latency
const user = users.get(id);
if (!user) throw new Error(`User ${id} not found`);
return user;
}
const server = createServer(async (req, res) => {
try {
const id = req.url?.split('/').pop() ?? '';
const user = await getUser(id);
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify(user));
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
const status = message.includes('not found') ? 404 : 500;
res.writeHead(status, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: message }));
}
});
server.listen(3000);What this demonstrates:
async route handlers return Promises - errors must be caught per requesttry/catch around await maps failures to HTTP status codesError instances preserves messages for clients and logsawait does not block other requests unless you await serially inside a lockasync function always returns a Promise - even if you return 42.await pauses the function until the Promise settles, scheduling the remainder as microtasks.async functions become rejected Promises.// Slow: sequential (latency adds up)
const a = await fetchA();
const b = await fetchB();
// Fast: parallel independent I/O
const [a, b] = await Promise.all([fetchA(), fetchB()]);// Typed Result pattern for expected failures (optional)
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
async function safeFetch(url: string): Promise<Result<unknown>> {
try {
const res = await fetch(url);
return { ok: true, value: await res.json() };
} catch (error) {
return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };
}
}try/catch or framework error hooks (Express 5 improves this).await in loops - for (const id of ids) await fetch(id) is slow. Fix: Promise.all with concurrency limit (p-limit).void doWork().catch(log) intentionally, or await.util.promisify or fs/promises.import() for heavy deps.| Alternative | Use When | Don't Use When |
|---|---|---|
Raw Promises .then() | Interop with callback-style libraries | Complex branching readability suffers |
Promise.allSettled | Partial success batches | All must succeed atomically |
| Reactive streams (RxJS) | Event composition over time | Simple request/response handlers |
| Sync code | Pure CPU on small data | Any I/O or network |
await suspends only the async function, not the thread. Other requests run until your continuation resumes as a microtask.
Wrap in try/catch and call next(err), or use a wrapper that forwards rejections to error middleware.
Yes in ESM ("type": "module"). Importers wait for the module to finish initializing.
Calling an async function without await or .catch() - rejections may become unhandled.
Use a pool library or batch ids into chunks instead of unbounded Promise.all on thousands of items.
Yes - all fs/promises methods return Promises suitable for await.
for await (const chunk of stream) consumes async iterables - common with streams and fetch bodies.
Yes when it performs I/O. Ensure the framework awaits middleware return values (Express 5, Fastify 5 do).
throw new Error('DB failed', { cause: originalErr });Preserves nested stack context in Node 24.
Generally yes with await. Deep callback mixes without promisify may still lose context.
Return values and thrown errors integrate with Fastify's reply lifecycle - still prefer explicit validation at boundaries.
Hot paths with hundreds of microtasks where a state machine is clearer - rare in typical CRUD APIs.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 19, 2026