Topic 13.3
Promises and async/await
In one line
A Promise<T> is a value that will arrive later. TypeScript tracks what it will hold, turns every async function's return into a Promise, unwraps with await and Awaited, and treats a caught error as unknown.
Think of it like this
A restaurant buzzer. You order, they give you a buzzer, and you go and sit down. The buzzer is not your food, it is a promise of food. Later it either lights up (your meal is ready) or flashes red (the kitchen ran out). You can't eat a buzzer. TypeScript's Promise<Meal> is that buzzer: it tells you what will come out, and it stops you from eating the buzzer.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Promise
- An object standing for a result that will arrive later, or fail.
- Pending / fulfilled / rejected
- The three states: still waiting, finished with a value, finished with an error.
- async function
- A function that always returns a promise and may use await inside.
- await
- Pause until a promise settles, then give its value (or throw its error).
- Awaited<T>
- The type you get after awaiting T, unwrapping promises all the way down.
- Promise.all
- Waits for every promise. Gives an array of results in order, or fails as soon as one fails.
- Promise.allSettled
- Waits for every promise and reports each outcome, never rejecting itself.
- unknown
- The type of a value you know nothing about yet. You must check it before use.
- useUnknownInCatchVariables
- A strict flag (TypeScript 4.4) that types the variable in catch as unknown instead of any.
Step by step
01async means: returns a promise
This function returns an object, but TypeScript says it returns Promise<{ id: number; name: string }>. The async keyword wraps the result.
That is why getUser(1).name is an error: a promise has no name. The name is inside the promise.
async function getUser(id: number) {
return { id, name: "Asha" };
}
// getUser: (id: number) => Promise<{ id: number; name: string }>02await unwraps one layer
await gives the T inside Promise<T>. Do it inside an async function, or at the top of a module (all examples here are modules, so top-level await works).
const user = await getUser(1);
console.log(user.name); // Asha03Promise.all keeps each type
A fixed list in gives a tuple out. The first slot is the first promise's type, and so on. That is why you can destructure it and still get exact types.
04Catching: the error is unknown
Anything can be thrown, so err is unknown. Check it. err instanceof Error is the usual check (Topic 4.4).
try {
await risky();
} catch (err) {
if (err instanceof Error) {
console.log(err.message);
} else {
console.log("not an Error:", err);
}
}05The forgotten await
A promise is an object, so it is always truthy. if (isAdmin(2)) is true for every user. TypeScript reports this one as TS2801.
Try it yourself
- 1
Predict the tuple
In the second example, add a fourth promise
later([1, 2], 5)to the list. Predict the type ofresultsfirst, then check. Then changePromise.alltoPromise.raceand see what the type of the result becomes: one promise settles first, so you get the unionstring | number | boolean | number[], not a tuple. - 2
Throw something else
In the third example, change
throw new Error(...)tothrow 42insiderisky. Which branch runs? Is there any type error? (No: TypeScript can't know what a function throws.) - 3
Fix the admin check
In the last example, change
if (isAdmin(2))toif (await isAdmin(2)). The error goes away and the wrong message stops printing.
Code & diagrams
getUser returns an object, yet the type says Promise of that object. pending is the buzzer, user is the food.
Console
(nothing was printed)Expected console output
true
AshaResults come back in the order of the list, not the order they finish (the string finishes first). allSettled narrows on status.
Console
(nothing was printed)Expected console output
2 TWO false
fulfilled ok
rejected boomJavaScript lets you throw a string, so TypeScript won't let you call err.message until you have checked. Both branches run in this example.
Console
(nothing was printed)Expected console output
Error: network down
not an Error: just a stringThis example turns useUnknownInCatchVariables off (strict also turns it on). The checker is silent, and the thrown string has no message.
Console
(nothing was printed)Expected console output
Uncaught TypeError: Cannot read properties of undefined (reading 'toUpperCase')User 2 is not an admin, yet the first if lets them in: a promise is always truthy. The compiler warns on the first if and not on the one with await.
Console
(nothing was printed)Expected console output
welcome, admin!Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Forget await
In the first example, change const user = await pending; to const user = pending;.
Myth vs fact
Myth
An async function returns the value I return.
Fact
It returns a promise of it. Its declared return type must be Promise<...>.
Myth
Promise.all returns an array of the union of the types.
Fact
For a fixed list it returns a tuple with one type per position.
Myth
catch (err) always gives an Error.
Fact
You can throw anything, and the type is unknown for that reason. Libraries also throw strings and plain objects.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Awaited<T>is defined inlib.es5.d.ts. The built-inPromise.alltypings use a mapped tuple overAwaited, which is why they preserve each position:{ -readonly [P in keyof T]: Awaited<T[P]> }. You'll write types like that in Phase 8. - ▸
TypeScript cannot track what a function throws. There is no
throwsclause in the type, so error types are documentation only. This is why Topic 13.5 shows aResult<T, E>type, which puts failure in the return type where the checker can see it. - ▸
A function returning
Promise<void>can be passed where a() => voidcallback is expected, and its rejection then goes nowhere. The same goes forasynccallbacks given toarray.forEach. Usefor...ofwithawait, orPromise.all(array.map(...)).
Remember this
- 1
The problem this solves: code that waits for something (a network call, a timer) is easy to misuse, for example by using the waiting object as if it were the result. TypeScript tracks what a promise will hold and where you still have to wait for it. A promise is JavaScript's object for a result that isn't ready yet: a network request, a timer, a file read. It is in one of three states: pending (waiting), fulfilled (finished with a value) or rejected (failed with a reason). In TypeScript the type is **
Promise<T>**, whereTis the type of the value if it succeeds.Promise<string>will give you text;Promise<void>will give you nothing, only the news that it finished. - 2
You get the value out with
await, inside anasyncfunction (or at the top level of a module).await promisepauses that function until the promise settles, then gives you theT. If it was rejected,awaitthrows the reason instead. The types follow exactly:awaiton aPromise<number>is anumber;awaiton a plainnumberis just thatnumber. - 3
An **
asyncfunction always returns a promise**, even if you writereturn 5. TypeScript knows this:async function f() { return 5; }has the return typePromise<number>. If you write the return type yourself it must be a promise (Promise<number>), never plainnumber. This is the number-one thing beginners trip on: calling an async function **withoutawaitgives you the buzzer, not the food**. - 4
**
Awaited<T>** is a built-in helper (Topic 10.2) that answers "what do I get after awaitingT?" It unwraps a promise, and keeps unwrapping if the promise holds another promise.Awaited<Promise<string>>isstring;Awaited<string>is alsostring. It is the right way to get a function's result type:Awaited<ReturnType<typeof getUser>>. TypeScript uses it internally forawaitandPromise.all. - 5
**
Promise.alltakes several promises and gives back one promise of an array of results, in the same order. TypeScript is clever here: when you pass a fixed list, the result is a tuple** (Topic 5.2), one type per position, soPromise.all([fetchUser(), fetchCount(), fetchOk()])gives[User, number, boolean], not(User | number | boolean)[]. If any promise rejects,Promise.allrejects immediately.Promise.allSettlednever rejects: it gives one result object per promise, withstatus: "fulfilled"and avalue, orstatus: "rejected"and areason. That is a discriminated union (Topic 4.5), so you narrow onstatus. - 6
**Errors in
catchareunknown.** In JavaScript you canthrowanything: anError, a string, a number, evenundefined. So the variable incatch (err)can't be safely assumed to be anError. Since TypeScript 4.4 the flaguseUnknownInCatchVariables(part ofstrict) types it asunknown, which forces you to check (err instanceof Error) before you readerr.message. Before that flag it wasany, which allowederr.messageand crashed when the thrown value was a string. A rejected promise's reason is the same: when youawait, it arrives incatchasunknown. - 7
Two real bugs are worth naming. A **forgotten
await** (callingisAdmin(id)and using the promise as if it were the answer: a promise is always truthy). TypeScript catches some of these (if (promise)is errorTS2801), and the lint ruleno-floating-promisescatches the rest. And unhandled rejection: a promise that fails with nobody listening crashes Node and logs a console error in browsers.
Explain it without notes
What type does an async function return, and why?
What does Awaited<T> do, and where is it useful?
Why does Promise.all([a, b]) have a tuple type, and how is Promise.allSettled different?
Why is the variable in catch typed unknown, and how do you use it safely?
Practice
Write async function loadAll(): Promise<[number, string]> using Promise.all on two helper promises and returning their results.
Write function errorMessage(err: unknown): string that returns err.message for an Error, the text for a string, and "unknown error" otherwise. Use it in a catch.
Trade-offs
- ↔
unknownincatchcosts a check on every catch, and gives you code that doesn't crash on a thrown string. If you only call your own code that always throwsError, a tiny helper likeerrorMessageremoves the repetition. - ↔
Promise.allis fast but all-or-nothing;Promise.allSettledgives every outcome but you must handle each one.
Done when you can
I can explain why an async function's type is
Promise<T>.I can use
await,AwaitedandPromise.alland say what type each gives.I can narrow
Promise.allSettledresults onstatus.I can handle a caught
unknownsafely.I can spot a forgotten
awaitand say what the compiler and a linter each catch.