Menu

TypeScript Async Await: Types, Errors and Parallel Calls

How async and await are typed in TypeScript: an async function returns Promise<T>, await unwraps it, errors are caught with try/catch, top-level await needs an ES module, and the difference between awaiting one by one, awaiting in parallel, and the forEach pitfall.

This page includes runnable editors - edit, run, and see output instantly.

An async function in TypeScript always returns a promise: if its body returns a string, its type is Promise<string>. Inside it, await unwraps a Promise<T> into a T.

The runtime behavior (what await pauses, the event loop) is JavaScript, covered in JavaScript async/await. TypeScript's part is the types in and out.

Return Types of Async Functions

The declared return type of an async function must be Promise<...>, even though the body returns the plain value. TypeScript infers it if you leave it off.

Writing async function count(): number is error TS1064 (The return type of an async function or method must be the global Promise<T> type. Did you mean to write 'Promise<number>'?). The utility type Awaited<T> gives the unwrapped type: Awaited<ReturnType<typeof count>> is number.

Forgetting await

A missing await leaves you holding a Promise<T> instead of a T. TypeScript catches most of these because the types no longer fit:

The if (ok) bug is real: a promise object is always truthy, so it granted access. TypeScript reports it as TS2801. Assigning the promise to a boolean variable, or reading a property the promise does not have, would also fail to compile. A call whose result you ignore (save(user);) is not caught; the typescript-eslint rule no-floating-promises covers that.

Error Handling with try/catch

A rejected promise makes await throw, so ordinary try/catch works. Under strict, the variable in catch is unknown, and you narrow it before reading .message:

throw inside an async function rejects its promise rather than throwing at the call site. More patterns, including custom error classes and returning results instead of throwing, are on the error handling page.

Top-Level await

await outside any function only works in an ES module. A file compiled as CommonJS (the case for these examples, and for Node projects without "type": "module") rejects it:

index.ts(2,14): error TS1309: The current file is a CommonJS module and cannot use 'await' at the top level.

Wrap the code in an async main function and call it, as every example on this page does. In an ES module project ("type": "module" in package.json with module set to node16 or nodenext, or module: "esnext" for a bundler), top-level await is allowed.

Sequential vs Parallel

Each await waits for its promise before the next line starts. For independent calls, start them all first and await them together with Promise.all:

In the sequential half, b cannot finish before a because it has not started. In the parallel half, d finishes first, and the total time is about the longest call rather than the sum. Promise.all still returns results in input order, typed as a tuple.

The forEach Pitfall

forEach ignores the promise an async callback returns, so nothing waits for the work:

for...of with await processes items one at a time; Promise.all with map runs them in parallel and waits for all. forEach does neither, and TypeScript does not warn, because a callback typed to return void accepts one that returns a promise.

Async Iteration with for await

for await...of loops over an async iterable, such as an async generator, awaiting each value:

async function* pages(total: number): AsyncGenerator<string[]> {
    for (let page = 1; page <= total; page++) {
        await new Promise((r) => setTimeout(r, 10));
        yield [`item ${page}a`, `item ${page}b`];
    }
}

async function main() {
    for await (const batch of pages(3)) {
        console.log(batch.join(", ")); // batch: string[]
    }
}
main();

The element type comes from the generator's AsyncGenerator<T> annotation, or from inference when you leave it off.

Frequently Asked Questions

What is the return type of an async function in TypeScript?

Always a promise. An async function that returns a number has the return type Promise<number>, and one that returns nothing has Promise<void>. Writing async function f(): number is error TS1064, which suggests Promise<number>.

How do I use await at the top level in TypeScript?

Top-level await only works in an ES module, with module set to es2022, esnext, system, preserve, or node16/node18/node20/nodenext in a file Node treats as ESM, and target at es2017 or higher. In a CommonJS file it is error TS1309. The portable fix is an async main function: async function main() { ... } main();.

How do I handle errors with async/await in TypeScript?

Wrap the await in try/catch. Under strict, the caught value is typed unknown, so narrow it first: if (e instanceof Error) console.log(e.message). A rejection that is never awaited or caught becomes an unhandled rejection.

How do I run async calls in parallel in TypeScript?

Start all the promises first, then await them together: const [a, b] = await Promise.all([loadA(), loadB()]). Writing await loadA(); await loadB(); runs them one after the other. Promise.all keeps each result's type in the resulting tuple.

Why does async not work inside forEach?

forEach calls the callback and ignores what it returns, so the promises from an async callback are never awaited: the loop finishes immediately and code after it runs before the work is done. Use for...of with await for one-by-one work, or await Promise.all(items.map(async (x) => ...)) for parallel work.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED