RizTech Academy logo
RizTech Academy
Asynchronous TypeScriptLesson 2 of 530 min

async/await, and the types it infers for you

async/await is how modern TypeScript actually writes asynchronous code — it makes promises read like ordinary sequential code, and TypeScript infers the types through it beautifully. This lesson is async/await and, crucially, the types it produces: what await gives you, what an async function returns, and the type mistakes it prevents.

await unwraps a promise

await pauses until a promise resolves and gives you the value inside — and TypeScript types the result as the promise's T:

async function loadName(): Promise<string> {
  const p: Promise<string> = Promise.resolve("Kavita");
  const name = await p;         // name is string — await unwrapped Promise<string> to string
  return name.toUpperCase();    // fine — name is a string now
}

await p on a Promise<string> gives a string. This is the fix for the "used a promise as a value" error from the last lesson: await is how you get the value out of the box, and TypeScript knows the result's exact type — await on a Promise<User> gives a User, ready to use with full type checking. Read await as "unwrap this promise and give me the value, of type T".

await can only be used inside an async function (or at the top level of a module) — it is a compile error otherwise, which is TypeScript reminding you that unwrapping a promise is an async operation.

async functions always return a promise

The mirror rule: an async function always returns a Promise, regardless of what you write inside. Return a string, and the function returns Promise<string>:

async function getName(): Promise<string> {
  return "Kavita";       // you return a string, but the function's type is Promise<string>
}

const result = getName();    // result is Promise<string> — NOT string
const name = await getName(); // name is string

getName "returns a string", but because it is async, its actual return type is Promise<string> — the value is wrapped. So getName() gives a Promise<string> (you must await it), while await getName() gives the string. TypeScript infers this — you can write async function getName() with no return annotation and TypeScript computes Promise<string> — but annotating : Promise<string> on a public async function documents it clearly. Every async function's return type is Promise<whatever you return>.

This wrapping is why the "forgot to await" bug happens: const name = getName() (no await) gives you a Promise<string>, not a string, and using it as a string fails — TypeScript catches it, saving you the [object Promise] in your UI.

Sequential await — reads like normal code

The beauty of async/await is that dependent async steps read like ordinary top-to-bottom code, fully typed at each step:

async function loadDashboard(userId: number): Promise<string> {
  const user = await fetchUser(userId);        // user is User
  const orders = await fetchOrders(user.id);   // orders is Order[] — uses user.id, typed
  const total = orders.reduce((sum, o) => sum + o.total, 0);   // total is number
  return `${user.name} spent ${total}`;
}

Each await unwraps its promise to a typed value, and the next line uses it with full type checking — user.id is checked, o.total is checked. No .then nesting, no callback pyramid; it reads like the sequential logic it is, and TypeScript follows the types through every step. This is why async/await replaced .then chains: same behaviour, far more readable, and the types flow naturally.

Sequential awaits run one after another — right when each step depends on the previous (you need the user before you can fetch their orders). When steps are independent, running them sequentially wastes time, and you use Promise.all to run them concurrently — the next lesson.

await on a non-promise, and Awaited

await on a value that is not a promise just gives you the value — harmless, occasionally useful when a value might or might not be a promise:

const a = await 42;          // a is number (await on a non-promise returns it as-is)

And the Awaited<T> utility type (from the utility-types lesson, built with infer) gives you the type await would produce — useful for typing "the resolved type of this async operation" without calling it:

type UserResult = Awaited<ReturnType<typeof fetchUser>>;   // the User type fetchUser resolves to

Awaited<ReturnType<typeof fetchUser>> is "the type you get after awaiting what fetchUser returns" — combining two utility types to derive a resolved type from an async function's signature. This is how you type a variable that will hold an awaited result without duplicating the type. Awaited also unwraps nested promises (a Promise<Promise<string>> awaits to string), which is why it exists rather than a simple infer.

Errors in async functions

A throw inside an async function becomes a rejected promise, and you handle it with ordinary try/catch around the await:

async function loadUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error(`Failed: ${response.status}`);   // becomes a rejected promise
  }
  return response.json();
}

async function main() {
  try {
    const user = await loadUser(1);
  } catch (error) {
    // error is `unknown` in strict mode — see the error-handling lesson
  }
}

try/catch around await catches both a rejected promise and a throw — the async-error-handling lesson covers the crucial detail that error is typed unknown and what to do about it. The point here is that async/await lets you use synchronous-looking error handling (try/catch) for asynchronous errors, which is far cleaner than .catch callbacks.

Check your work

What await does, and its result type. Unwraps a promise to the value inside, typed as the promise's T — await Promise<User> gives a User.

Where await can be used. Only inside an async function or at a module's top level.

What an async function always returns. A Promise — Promise<whatever you return>, even if you return a plain value.

Why the "forgot to await" bug happens, and who catches it. Calling an async function without await gives a promise, not the value; TypeScript catches the misuse.

How dependent async steps read. Like ordinary sequential code — each await gives a typed value the next line uses, no nesting.

When sequential await is right, and when it wastes time. Right when a step depends on the previous; wasteful when steps are independent (use Promise.all — next lesson).

What Awaited<T> gives. The type await would produce — deriving a resolved type from an async signature, and unwrapping nested promises.

How async errors are handled. A throw becomes a rejected promise, caught by try/catch around the await (with error typed unknown).

Practice

  1. Write an async function that awaits a Promise<string> and confirm the awaited value is a string.
  2. Write async function getName(): Promise<string> { return "Kavita" }. Confirm getName() is a promise and await getName() is a string.
  3. Call an async function without await, use the result as if it were the value, and read the error.
  4. Write a loadDashboard with three dependent awaits and confirm each intermediate value is typed.
  5. await a non-promise value and confirm you get the value.
  6. Use Awaited<ReturnType<typeof someAsyncFn>> to type a variable and confirm it is the resolved type.
  7. Wrap an await in try/catch, throw inside the async function, and confirm the catch runs (note error is unknown — next lesson).

Official documentation

Next: running work concurrently with the promise combinators.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship