ZetCode

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.

basic.ts
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.

missing_await.ts
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).

top_level.ts
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.

errors.ts
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.

parallel.ts
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.

all_settled.ts
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.

timeout.ts
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.

async_iteration.ts
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.

awaited.ts
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.

floating.ts
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

Source

MDN: async function

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all TypeScript tutorials.