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
- Write an
asyncfunction thatawaits aPromise<string>and confirm the awaited value is astring. - Write
async function getName(): Promise<string> { return "Kavita" }. ConfirmgetName()is a promise andawait getName()is a string. - Call an async function without
await, use the result as if it were the value, and read the error. - Write a
loadDashboardwith three dependentawaits and confirm each intermediate value is typed. awaita non-promise value and confirm you get the value.- Use
Awaited<ReturnType<typeof someAsyncFn>>to type a variable and confirm it is the resolved type. - Wrap an
awaitintry/catch, throw inside the async function, and confirm the catch runs (noteerrorisunknown— next lesson).
Official documentation
- TypeScript — async/await — The syntax and its types.
- MDN — async function — Behaviour, from the source.
- TypeScript — Awaited — Deriving resolved types.
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