Most application code is asynchronous; getting Promise types and error handling right avoids silent unsafety.
An async function always returns a Promise. Its declared return type is Promise<T>, and inside you return a plain T:
async function getCount(): Promise<number> { return 42; }
const n = await getCount(); // numberPromise<T> is generic: .then callbacks receive T, await unwraps it, and Promise.all([a, b]) returns a tuple of each result type. Awaited<T> is the type-level version of await, useful with ReturnType on async functions.
Errors are untyped. A promise's type says nothing about how it can fail, and with strict (useUnknownInCatchVariables) a catch (e) variable is unknown. Narrow it before use: e instanceof Error ? e.message : String(e). The .catch(err => ...) callback's parameter is typed any by the standard library, so annotate it as unknown yourself.
External data is a trust boundary. fetch(...).then(r => r.json()) resolves to any. Writing const user: User = await res.json() compiles, but it's an unchecked claim. Type it as unknown and validate (by hand or with a schema library such as Zod), or at least centralise the cast in one well-named function so the risk is visible.
Forgotten awaits are a classic bug: if (isAdmin(user)) where isAdmin is async is always truthy because a promise is an object. TypeScript flags some of these (a promise in a condition when the function is known to return one), and the @typescript-eslint/no-floating-promises and no-misused-promises lint rules catch the rest — enable them.
Use void for async functions that return nothing: Promise<void>.
Example
interface User { id: number; name: string }
function isUser(x: unknown): x is User {
return typeof x === "object" && x !== null &&
"id" in x && typeof x.id === "number" &&
"name" in x && typeof x.name === "string";
}
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`https://api.example.com/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const body: unknown = await res.json(); // don't trust `any`
if (!isUser(body)) throw new Error("Unexpected response shape");
return body;
}
async function main(): Promise<void> {
try {
const [a, b] = await Promise.all([fetchUser(1), fetchUser(2)]); // [User, User]
console.log(a.name, b.name);
} catch (e) { // e: unknown
console.error(e instanceof Error ? e.message : String(e));
}
}
main();Remember
- Async functions return
Promise<T>;awaitunwraps toT - Caught errors are
unknown— narrow before use res.json()isany: type itunknownand validate- Lint for floating and misused promises
Watch out for
- Annotating
await res.json()as your model type and treating it as validated.