async / await
The async/await syntax lets you write asynchronous code that reads top to bottom like synchronous code, while still returning promises under the hood. TypeScript fully understands this: an async function always returns a Promise<T>, and await unwraps a Promise<T> back to a T within the function body. This page covers how to type async functions, what await does to the type of an expression, the difference between sequential and concurrent awaits, async arrow functions, and typing for await...of loops.
Declaring async functions
Marking a function async changes its return type: whatever you return is wrapped in a Promise. Annotate the resolved type with : Promise<T> to make the contract explicit and catch wrong returns inside the body.
async function fetchTitle(url: string): Promise<string> {
const res = await fetch(url);
const data = await res.text();
return data; // must be a string (or Promise<string>)
}
// Return type inferred as Promise<number>
async function add(a: number, b: number) {
return a + b;
}
Inside fetchTitle, you return a string but the function’s external type is Promise<string>. The compiler verifies the returned value matches the annotation, so accidentally returning the wrong type fails right where the mistake is.
What await does to a type
The await operator unwraps a promise: applying it to a Promise<T> yields a value of type T. Applied to a non-promise it returns the value unchanged, matching Awaited<T> semantics.
async function demo() {
const n: number = await Promise.resolve(7); // Promise<number> -> number
const s: string = await "literal"; // string -> string (no-op await)
const res = await fetch("/api/data");
const json = await res.json(); // any -- json() returns Promise<any>
}
Because await only works inside async functions (or top-level in ES modules), using it elsewhere is a compile error. Note that res.json() resolves to any; you must add a type yourself, which the Typed API Responses page covers in depth.
Top-level
awaitis allowed in ES modules ("module": "esnext"/"es2022"and up intsconfig.json). It is not available in CommonJS output.
Sequential vs concurrent awaits
Awaiting promises one after another runs them sequentially, which is slower when they are independent. Kick off all the promises first, then await them together with Promise.all to run concurrently.
// ❌ Slow: each await blocks the next (total ≈ sum of durations)
async function sequential() {
const user = await getUser();
const posts = await getPosts();
return { user, posts };
}
// ✅ Fast: both start immediately, run in parallel (total ≈ max duration)
async function concurrent() {
const userP = getUser(); // Promise<User>
const postsP = getPosts(); // Promise<Post[]>
const [user, posts] = await Promise.all([userP, postsP]);
return { user, posts };
}
In concurrent, calling the functions without await starts both immediately; the single Promise.all then waits for both. The destructured tuple keeps each value’s exact type. Only force sequencing when one call genuinely depends on a previous result.
Async arrow functions and methods
The async keyword works on arrow functions, object methods, and class methods. Type the resolved value with the usual Promise<T> return annotation.
const loadConfig = async (): Promise<Config> => {
const res = await fetch("/config.json");
return res.json() as Promise<Config>;
};
class UserService {
async findById(id: string): Promise<User | null> {
const row = await db.query(id);
return row ?? null;
}
}
Async arrows are common in callbacks and React effects. Returning Promise<User | null> documents that the method may resolve to null, forcing callers to handle the missing case after they await.
Async iteration with for await…of
For sources that produce values over time — async generators, paginated APIs, streams — use for await...of. TypeScript types the loop variable from the AsyncIterable<T> element type.
async function* pages(): AsyncGenerator<number[]> {
yield [1, 2];
yield [3, 4];
}
async function sumAll() {
let total = 0;
for await (const batch of pages()) {
// batch: number[]
total += batch.reduce((a, b) => a + b, 0);
}
return total;
}
The generator’s return type AsyncGenerator<number[]> flows into batch, which is inferred as number[]. Async iteration requires "target": "es2018" or later, and "lib" including esnext.asynciterable.
Common patterns at a glance
| Goal | Pattern |
|---|---|
| Run dependent calls | sequential await statements |
| Run independent calls | start all, then await Promise.all([...]) |
| Handle one failure of many | await Promise.allSettled([...]) |
| Wrap a callback API | new Promise<T>((resolve, reject) => ...) |
| Add a timeout | Promise.race([work, timeout]) |
Best Practices
- Always annotate exported async functions with
: Promise<T>for a clear contract. - Start independent promises first, then
await Promise.allinstead of awaiting sequentially. - Reserve sequential
awaitfor calls that truly depend on a previous result. - Remember
awaitonly unwraps insideasyncfunctions or top-level ESM. - Type the result of
res.json()yourself — it isanyby default. - Use
for await...offor streams and paginated sources rather than manual recursion. - Avoid mixing
.then()chains withawaitin the same function; pick one style for readability.