Typing async code

TypeScript · Practical · 10 min

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(); // number

Promise<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>; await unwraps to T
  • Caught errors are unknown — narrow before use
  • res.json() is any: type it unknown and validate
  • Lint for floating and misused promises

Watch out for

  • Annotating await res.json() as your model type and treating it as validated.

Check yourself

  1. What is the type of await Promise.all([Promise.resolve(1), Promise.resolve("a")])?
  2. With strict enabled, what is the type of e in try { ... } catch (e) { }?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html

Builds on

Unlocks

Nothing builds on this yet.