Q17 of 38 · TypeScript

How do you correctly type async functions and Promise values in TypeScript?

TypeScriptMidtypescriptasyncpromisesawaitedreturn-typesgenerics

Short answer

Short answer: An `async` function's return type is `Promise<T>`. Use `Awaited<T>` (TypeScript 4.5+) to unwrap the resolved type of a Promise generically. Annotate return types on public async functions for clarity; TypeScript infers them on private ones. Handle errors with `unknown` in catch blocks under strict mode.

Detail

Typing async code correctly prevents a class of runtime bugs where a resolved value is treated as its raw Promise.

Async function return type: Any function declared async implicitly returns Promise<T> where T is the type of the resolved value. If you annotate async function fetch(): User, TypeScript will silently wrap it in Promise<User>. To annotate explicitly: async function fetch(): Promise<User>.

Awaited: The utility type for unwrapping a Promise generically. Awaited<Promise<string>> is string; Awaited<Promise<Promise<string>>> is also string (recursively unwrapped). Use it in generic helper types: type Result<F extends () => Promise<unknown>> = Awaited<ReturnType<F>>.

Promise in function signatures: Returning Promise<void> signals the caller must await but the resolved value is unused. Promise<never> means the function always rejects or never resolves.

Error typing in catch: Under strict mode (useUnknownInCatchVariables), err in catch is unknown. Always narrow: if (err instanceof Error) { ... }.

Typed Promise chains: .then() and .catch() callbacks are typed by the Promise's generic parameter — TypeScript infers the handler argument automatically.

// EXAMPLE

// Return type annotation on async function
async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  return res.json() as Promise<User>;
}

// Awaited — unwrap resolved type
type FetchResult = Awaited<ReturnType<typeof fetchUser>>; // User

// Promise<void> — caller should await but result unused
async function cleanup(): Promise<void> {
  await db.disconnect();
}

// Generic async helper
async function retry<T>(
  fn: () => Promise<T>,
  times: number
): Promise<T> {
  let lastErr: unknown;
  for (let i = 0; i < times; i++) {
    try { return await fn(); }
    catch (e) { lastErr = e; }
  }
  throw lastErr;
}

// Catch with unknown
try {
  await fetchUser(1);
} catch (err) {
  const message = err instanceof Error ? err.message : String(err);
  console.error(message);
}

// WHAT INTERVIEWERS LOOK FOR

`Promise<T>` return type annotation, `Awaited<T>` for generic unwrapping, `Promise<void>` semantics, and the catch `unknown` pattern. Candidates who demonstrate `Awaited<ReturnType<...>>` for inferred types show advanced practical TypeScript.

// COMMON PITFALL

Annotating an async function's return type as `User` instead of `Promise<User>` — TypeScript will wrap it anyway, but the annotation is misleading for callers. Always include `Promise<>` in explicit async return types.