Async Code & Promises
Almost every real program waits on something — a network request, a file, a timer. TypeScript types asynchronous code with Promise<T>, where T is the value you'll eventually get, and checks async/await code just as carefully as synchronous code.
Promise<T>
function delay(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } function rollDie(): Promise<number> { return delay(10).then(() => 4); } rollDie().then(n => console.log(`rolled ${n}`, typeof n));
rolled 4 number
Promise<number> means "an object that will eventually produce a number". Inside .then, n is typed number automatically.
async functions
An async function always returns a promise. TypeScript wraps whatever you return in Promise<…>:
interface User { id: number; name: string; } async function getUser(id: number): Promise<User> { return { id, name: "Ana" }; } async function main() { const user = await getUser(1); console.log(user.name); const notAwaited = getUser(2); console.log(notAwaited.name); }
error TS2339: Property 'name' does not exist on type 'Promise<User>'.
Forgetting await is one of the most common async bugs. TypeScript catches it here because a Promise<User> has no name property. The return type of an async function must be written as Promise<T> — writing just User is an error.
Typing fetch responses
fetch knows it returns a Response, but response.json() returns Promise<any> — TypeScript has no idea what the server sends. Give the result a type at the boundary:
interface Todo { id: number; title: string; completed: boolean; } async function loadTodo(id: number): Promise<Todo> { const res = await fetch(`https://example.com/todos/${id}`); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } return (await res.json()) as Todo; }
The as Todo is a promise you are making — nothing checks that the server actually returned that shape. For data you don't control, validate it at runtime (with a type guard, or a schema library like Zod) before trusting the type. Lesson 37 shows how.
A reusable typed helper
async function getJson<T>(url: string): Promise<T> { const res = await fetch(url); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); return res.json() as Promise<T>; } const todos = await getJson<Todo[]>("/api/todos"); todos.forEach(t => console.log(t.title));
A generic helper like this centralises error handling and lets each caller say what shape it expects.
Running promises in parallel
Promise.all keeps the type of each position in the tuple you pass it:
const wait = <T>(value: T, ms: number) => new Promise<T>(r => setTimeout(() => r(value), ms)); async function dashboard() { const [user, unread, flags] = await Promise.all([ wait({ name: "Ola" }, 30), wait(7, 10), wait(["beta"], 20), ]); console.log(user.name, unread + 1, flags.includes("beta")); } dashboard();
Ola 8 true
user is { name: string }, unread is number and flags is string[] — each awaited result keeps its own type. Running independent requests together like this is much faster than awaiting them one after another.
Promise.allSettled never rejects; it gives an array of results that you narrow by status:
async function main() { const results = await Promise.allSettled([ Promise.resolve("ok"), Promise.reject(new Error("nope")), ]); for (const r of results) { if (r.status === "fulfilled") console.log("value:", r.value); else console.log("reason:", (r.reason as Error).message); } } main();
value: ok reason: nope
Async iteration
Async generators produce values over time, and for await consumes them. The type is AsyncGenerator<T>:
async function* pages(total: number): AsyncGenerator<number[]> { for (let p = 0; p < total; p++) { await new Promise(r => setTimeout(r, 5)); yield [p * 2 + 1, p * 2 + 2]; } } async function main() { for await (const page of pages(3)) { console.log(page); } } main();
[ 1, 2 ] [ 3, 4 ] [ 5, 6 ]
This is a natural fit for paginated APIs, streaming responses and reading large files line by line.
Floating promises
Calling an async function without awaiting it or handling its rejection is a "floating promise": if it fails, the error may go unnoticed. TypeScript itself doesn't flag this, but the typescript-eslint rule no-floating-promises does (lesson 41). Either await the call, return it, or attach a .catch().