TypeScript async/await
last modified October 15, 2026
Asynchronous code works with values that are not available yet,
such as the result of a network request or a file read. In JavaScript, such a
value is represented by a Promise. The async and
await keywords let us write asynchronous code that reads like
sequential code. TypeScript adds types: a Promise<T>
resolves to a value of type T, and await unwraps
it.
The examples simulate slow operations with a delay
function based on setTimeout, so they run without network
access.
An async function
An async function always returns a promise. Its return
type is written as Promise<T>, where T is the
type of the returned value.
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function getUserName(id: number): Promise<string> {
await delay(100);
return id === 1 ? "Alice" : "Bob";
}
async function main(): Promise<void> {
console.log("fetching user...");
const name = await getUserName(1);
console.log(`user: ${name}`);
}
main();
console.log("main started");
The getUserName function returns a
string, which TypeScript wraps in Promise<string>.
The await expression has the type string. Note the
order of the output: main runs until the first
await, and the last line executes before the user name
arrives.
$ npx tsx basic.ts fetching user... main started user: Alice
Type errors with promises
A promise is not the value it resolves to. Forgetting
await is a common mistake, and the compiler catches it.
async function getPrice(): Promise<number> {
return 25;
}
async function main(): Promise<void> {
const price = getPrice();
console.log(price.toFixed(2));
}
main();
The price variable is a
Promise<number>, which has no toFixed
method. Writing await getPrice() fixes it.
$ npx tsc --noEmit missing_await.ts(7,23): error TS2339: Property 'toFixed' does not exist on type 'Promise<number>'.
Top-level await
In ES modules, await can be used at the top level of a
file, outside of any function. This requires "module" set to
es2022, esnext, or nodenext in
tsconfig.json, and a module file (with
"type": "module" in package.json for Node.js).
const response = await fetch("https://example.com");
const html = await response.text();
console.log(response.status);
console.log(html.match(/<title>(.*?)<\/title>/)?.[1]);
The global fetch function is available in browsers and
in Node.js 18 and later. It returns Promise<Response>, and
the text method returns Promise<string>, so
html is a string. The json method, used
for JSON APIs, returns Promise<any>; its result is best
assigned to unknown and checked before use.
$ npx tsx top_level.ts 200 Example Domain
Error handling
A rejected promise makes await throw. The error is
caught with try/catch. In strict mode, the caught
value has the unknown type, because any value can be thrown.
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function loadConfig(name: string): Promise<Record<string, string>> {
await delay(50);
if (name !== "app") {
throw new Error(`config '${name}' not found`);
}
return { theme: "dark", language: "en" };
}
async function main(): Promise<void> {
for (const name of ["app", "db"]) {
try {
const config = await loadConfig(name);
console.log(name, config);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
console.log(`error: ${message}`);
} finally {
console.log(`done with ${name}`);
}
}
}
main();
Throwing inside an async function rejects the returned
promise. The finally block runs in both cases.
$ npx tsx errors.ts
app { theme: 'dark', language: 'en' }
done with app
error: config 'db' not found
done with db
Sequential and parallel execution
Awaiting tasks one by one runs them sequentially. Independent tasks
can run in parallel with Promise.all, which also preserves the
type of each result.
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function getUser(): Promise<{ name: string }> {
await delay(200);
return { name: "Alice" };
}
async function getOrderCount(): Promise<number> {
await delay(200);
return 3;
}
async function main(): Promise<void> {
let start = Date.now();
const user1 = await getUser();
const count1 = await getOrderCount();
console.log(user1.name, count1, `sequential: ~${Math.round((Date.now() - start) / 100) * 100} ms`);
start = Date.now();
const [user2, count2] = await Promise.all([getUser(), getOrderCount()]);
console.log(user2.name, count2, `parallel: ~${Math.round((Date.now() - start) / 100) * 100} ms`);
}
main();
The sequential version waits for both delays one after the other;
the parallel one waits only once. The result of Promise.all is a
tuple of type [{ name: string }, number], so destructuring gives
each variable its own type. If any of the promises rejects,
Promise.all rejects as well.
$ npx tsx parallel.ts Alice 3 sequential: ~400 ms Alice 3 parallel: ~200 ms
Promise.allSettled
When some tasks may fail and the others should still be used,
Promise.allSettled waits for all of them and reports the outcome
of each one.
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function check(host: string): Promise<number> {
await delay(50);
if (host.endsWith(".invalid")) {
throw new Error(`cannot reach ${host}`);
}
return host.length * 10;
}
async function main(): Promise<void> {
const hosts = ["example.com", "down.invalid", "example.org"];
const results = await Promise.allSettled(hosts.map(check));
results.forEach((result, i) => {
if (result.status === "fulfilled") {
console.log(`${hosts[i]}: ${result.value} ms`);
} else {
const reason = result.reason instanceof Error ? result.reason.message : result.reason;
console.log(`${hosts[i]}: failed (${reason})`);
}
});
}
main();
Each result is a discriminated union. Checking
status narrows it to a fulfilled result with
value or a rejected one with reason.
$ npx tsx all_settled.ts example.com: 110 ms down.invalid: failed (cannot reach down.invalid) example.org: 110 ms
Timeouts with Promise.race
The Promise.race method settles with the first promise
that settles. It can be used to give up on an operation that takes too
long.
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
function timeout(ms: number): Promise<never> {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error(`timed out after ${ms} ms`)), ms));
}
async function slowTask(ms: number): Promise<string> {
await delay(ms);
return `finished in ${ms} ms`;
}
async function withTimeout<T>(task: Promise<T>, ms: number): Promise<T> {
return Promise.race([task, timeout(ms)]);
}
async function main(): Promise<void> {
for (const duration of [100, 500]) {
try {
console.log(await withTimeout(slowTask(duration), 300));
} catch (e) {
console.log(e instanceof Error ? e.message : e);
}
}
}
main();
The timeout function returns
Promise<never>, because it never resolves with a value. The
race of Promise<T> and Promise<never>
therefore has the type Promise<T>. Note that the slow task
is not cancelled; its result is just ignored. Real cancellation is done with
an AbortController.
$ npx tsx timeout.ts finished in 100 ms timed out after 300 ms
Async iteration
An async generator produces values over time. They are consumed
with the for await...of loop.
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function* readPages(count: number): AsyncGenerator<string[]> {
for (let page = 1; page <= count; page++) {
await delay(50);
yield [`item ${page * 2 - 1}`, `item ${page * 2}`];
}
}
async function main(): Promise<void> {
for await (const items of readPages(3)) {
console.log(items.join(", "));
}
}
main();
The readPages generator simulates a paged API. Its
return type AsyncGenerator<string[]> makes
items a string[] in the loop.
$ npx tsx async_iteration.ts item 1, item 2 item 3, item 4 item 5, item 6
The Awaited type
The Awaited utility type gives the type a promise
resolves to. Combined with ReturnType, it extracts the result
type of an async function.
async function loadProfile() {
return { id: 7, name: "Alice", roles: ["admin", "editor"] };
}
type Profile = Awaited<ReturnType<typeof loadProfile>>;
function describe(profile: Profile): string {
return `${profile.name} (${profile.roles.join(", ")})`;
}
async function main(): Promise<void> {
const profile = await loadProfile();
console.log(describe(profile));
}
main();
The loadProfile function has no return type
annotation; its inferred type is a promise of an object.
ReturnType gives that promise type, and Awaited
unwraps it to the object type, which can then be used elsewhere without
repeating its definition.
$ npx tsx awaited.ts Alice (admin, editor)
Floating promises
A promise that is neither awaited nor handled is called a floating promise. If it rejects, the error is reported as an unhandled rejection, and in Node.js the process exits.
async function save(data: string): Promise<void> {
if (data.length === 0) {
throw new Error("nothing to save");
}
}
async function main(): Promise<void> {
await save("report");
console.log("saved");
save("").catch((e: unknown) => {
console.log("background save failed:", e instanceof Error ? e.message : e);
});
}
main().catch(e => console.error("unexpected error:", e));
The second call is intentionally not awaited, so it gets its own
catch handler. The top-level call to main is
handled the same way. TypeScript itself does not report floating promises; the
no-floating-promises rule of typescript-eslint does.
$ npx tsx floating.ts saved background save failed: nothing to save
Best Practices
- Annotate Return Types: Write
Promise<T>on exported async functions to document what they resolve to. - Run Independent Tasks in Parallel: Use
Promise.allinstead of awaiting unrelated operations one by one. - Treat Caught Errors as unknown: Check
e instanceof Errorbefore usingmessage. - Never Leave Promises Floating: Await every promise or attach a
catchhandler. - Derive Types with Awaited: Use
Awaited<ReturnType<typeof fn>>instead of duplicating result types.
Source
This tutorial covered asynchronous programming with async/await in
TypeScript. The compiler tracks the types of resolved values through
await, Promise.all, and Awaited.
Author
List all TypeScript tutorials.