Skip to content
Theory · Async code and outside data · lesson 1 of 10

Promise<T> and async

What you'll learn

What you'll learn:

  • read the type Promise<T> and explain why an async function always returns a promise;
  • get a value of type T with await and recognize a forgotten await from a tsc message;
  • type Promise.all and the error in catch, whose type is unknown;
  • check data from outside with a type predicate and tell that check apart from as.
A Promise receipt on the counter turns into a finished dish flagged T through the await hatch

At a café you place an order and get a receipt with a number in return. The dish does not exist yet: the kitchen is cooking it. The receipt is a promise: when the dish is ready, you get it for this receipt. You cannot eat the receipt; you can only exchange it for the dish at the pickup window.

In JavaScript that receipt is a promise. A function that takes a while to get data, for example from a server, returns a promise at once, and the value itself arrives later. TypeScript writes on the receipt which dish it is for: Promise<User> is a receipt for a user.

What happens if you mix up the receipt and the dish? The function loadUser returns a promise, and the code reads a name from the result right away:

TypeScript
type User = { name: string };

async function loadUser(): Promise<User> {
    return { name: "Anna" };
}

async function main() {
    const user = loadUser();
    console.log(user.name); // ✗
}

main();
TEXT
error TS2339: Property 'name' does not exist on type 'Promise<User>'.

The message says that the type Promise<User> has no property name. user holds the receipt, not the user. Without type checking the program would not crash: it would quietly print undefined.

The type Promise<T>

As you already know from the generics theme, Promise<T> is a generic type. The angle brackets hold the type of the value the promise gives back when it finishes:

  • Promise<number> gives back a number;
  • Promise<User> gives back an object of type User;
  • Promise<void> gives back nothing useful and only reports that the work is done.

The promise itself and the value inside it are different things. A promise has the methods then and catch, but no user properties. That is why tsc refused to read user.name.

An async function returns a promise

The word async in front of a function changes its result type. Whatever stands in return, the calling code gets a promise holding that value:

  • a function with return 41 returns Promise<number>;
  • a function with no return returns Promise<void>.

This holds even when the value is ready at once. The café still hands you a receipt, though the dish is already on the counter.

That is why the result type of an async function is written as a promise. If you write the type of the value itself, tsc stops you:

TypeScript
async function loadCount(): number { // ✗
    return 41;
}
TEXT
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>'?

At the end of the message tsc suggests the right form: Promise<number>. You may also leave the result type out: TypeScript infers Promise<number> from return 41 on its own.

await unwraps the promise

To get the dish, you exchange the receipt at the pickup window. In code that is what await does: it waits until the promise finishes and gives back the value. For TypeScript this means that await turns Promise<T> back into T:

TypeScript
type User = { name: string };

async function loadUser(): Promise<User> {
    return { name: "Anna" };
}

async function main() {
    const user = await loadUser();
    console.log(user.name); // Anna
}

main();

Let's walk through the example line by line:

  1. loadUser is declared with the result type Promise<User>.
  2. await loadUser() waits for the promise, so user has the type User, not Promise<User>.
  3. user.name passes the check, and the program prints Anna.
  4. await sits inside the async function main, and main() is called at the end of the file: that way the example works in any file.

The widget shows the same value with await and without it. Watch the tooltip: without await the value has a promise type, with await it is number, and tsc does not let a forgotten await through in arithmetic.

LiveA promise and awaitstep 1 of 6

The body returns 41, but async wraps the result in Promise<number>.

Press "Next step": the hover shows a type or an error on the highlighted line

The TS2365 message from the fifth step is worth remembering: the operator + cannot be applied to Promise<number> and 1. It says '1', not 'number': TypeScript names a literal by its exact type. If an error message shows Promise<…> where you expected a value, an await is almost always missing.

Here the receipt analogy stops working. At the pickup window a real receipt is taken from you, but the promise stays with you. If you write await for the same promise a second time, it gives back the same value at once.

Interviewers like to ask which type an async function returns. The answer: always Promise<T>, where T is the type of what stands in return, and await gives a value of type T.

Common mistake

The await is forgotten in a condition:

TypeScript
async function isAdmin(): Promise<boolean> {
    return false;
}

async function main() {
    if (isAdmin()) { // ✗
        console.log("admin panel");
    }
}

main();
TEXT
error TS2801: This condition will always return true since this 'Promise<boolean>' is always defined.

The message says that the condition is always true because the promise always exists. The receipt is an object, and an object in an if always counts as true, whatever you later get for it. Without type checking the program would show the admin panel to everyone, though isAdmin returns false. You need to wait for the answer: if (await isAdmin()).

Key points

  • Promise<T> is a promise that gives back a value of type T; the promise itself has none of that value's properties.
  • An async function always returns Promise<T>, and its result type is written as Promise<number>, not number.
  • await unwraps Promise<T> back into T.
  • Promise<…> in a TS2339, TS2365 or TS2801 message where a value was expected almost always means a forgotten await.