skip to content

In TypeScript, what return type is inferred for `async function loadCount() { return 1; }`, and what happens if you annotate that function's return type as `number`?

level: middleimportance: should knowfreq 44%

answer

  1. the keyword changes the signature
  2. the body returns the resolved value
  3. one wrapper, never two
  4. no value at all still has a type
  5. nothing in it describes rejection

basics

~10 s

The inferred return type is Promise<number>. Annotating it as number is a compile error: an async function's declared return type must be a promise type, so the correct annotation is Promise<number>.

solid answer

~50 s

`loadCount` is typed `() => Promise<number>`. The `async` keyword is what makes it so: whatever the body returns, the function hands back a promise resolving to that type, and TypeScript models exactly that. Annotating `: number` fails, with the compiler telling you the return type of an async function must be a promise type and suggesting `Promise<number>`. The half people miss is that promises do not nest — if the body returns a `Promise<number>`, the function is still `Promise<number>`, not `Promise<Promise<number>>`, because resolving a promise with a promise adopts its state and the type system models the same flattening. An async function that returns no value is `Promise<void>`. And the annotation participates in checking like any other return type: a body resolving to the wrong shape is an error at the function, not at the caller.

code

typescript · 10 lines
typescript
async function loadCount(): Promise<number> {
  return 1;
}

async function loadTwice(): Promise<number> {
  const n = await loadCount();
  return Promise.resolve(n * 2);
}

loadTwice().then((n) => console.log(n));

go deeper

for a junior

Remember that an async function's type is always a promise of what the body returns, so the annotation is Promise<number> where the body returns a number.

for a middle

Explain the rule and its edges: a non-promise annotation is rejected outright, a promise-returning body flattens to a single wrapper, and a valueless body gives Promise<void>.

for a senior

Point out what the signature cannot express — rejection types are absent and the catch variable is any or unknown — and design contracts that put failure in the resolved type when that matters.

for a principal

Own the boundary convention: whether async operations across your services signal failure by rejection or by a modelled result type, and what each choice costs in call-site ergonomics and error-handling discipline.

## What `async` does to the signature Marking a function `async` changes its type, not just its body. The declared or inferred return type of an async function is always a promise: the compiler takes the union of the return expressions' types, unwraps any promise among them, and wraps the result once. ```ts async function loadCount() { return 1; } // inferred: () => Promise<number> ``` So the value you `return` inside the body is the *resolved* type, and the function's own type sits one level higher. This is the most common confusion in the area: people read the body, see `return 1`, and write `: number`. ## Why `: number` is rejected TypeScript refuses a non-promise annotation on an async function outright. The message says the return type of an async function or method must be the global `Promise<T>` type, and it suggests the promise form. This is a rule about `async` itself, not an ordinary assignability complaint — it fires before any question of whether the body matches. ```ts async function loadCount(): Promise<number> { // correct return 1; } ``` The annotation needs the global `Promise` type to be in scope, which is what your `lib` setting provides. In an environment whose library files do not declare it, the same annotation fails for a different reason: the type is simply not there. ## Promises do not nest The second half of the rule matters more in real code: ```ts async function fetchCount(): Promise<number> { return Promise.resolve(1); // still Promise<number> } ``` Returning a promise from an async function does not produce `Promise<Promise<number>>`. At runtime, resolving a promise with another promise adopts its state; the type system models the same flattening. This is why `return someAsyncCall()` and `return await someAsyncCall()` have the same declared type — the difference between them concerns error handling and stack behaviour at runtime, which is the JavaScript side of the fence, not the annotation. The same flattening governs `await`: awaiting a `Promise<number>` gives `number`, and awaiting a plain `number` also gives `number`. ## The everyday cases ```ts async function save(row: { id: string }): Promise<void> { await write(row); // no value returned } async function findUser(id: string): Promise<User | null> { const row = await get(id); return row ?? null; } ``` - No returned value gives `Promise<void>`. - A body with several returns unions the resolved types and wraps once: `Promise<User | null>`. - A body that only ever throws still has a promise type; the rejection is not represented in the type system at all, which is the important limitation below. ## What the annotation does *not* say The promise type has one type parameter, and it is the *resolved* type. There is no place in the signature to declare what a rejection carries. `Promise<number>` tells you nothing about failure modes, and a `catch` clause's variable is `any` — or `unknown` when `useUnknownInCatchVariables` is enabled, which `strict` turns on. If failure modes are part of your contract, they must be modelled in the resolved type itself, for example as a discriminated result object rather than as a thrown error. That is also the honest answer to "does the annotation make the function safe?": it constrains the success path and says nothing about the failure path. ## Non-async functions returning promises A function need not be `async` to return a promise, and the promise-only annotation rule does not apply to it: ```ts function loadCount(): Promise<number> { return Promise.resolve(1); // no async keyword, ordinary return-type checking } ``` From a caller's point of view the two versions of `loadCount` are indistinguishable — same type, same usage. `async` is an implementation choice that lets you use `await` inside; it is not part of the contract. That is worth saying out loud in an interview, because it explains why you can make a function `async` later without changing its declared type, as long as the resolved type stays the same. ## Erasure, once more The `Promise<number>` annotation is erased at compile time. Nothing verifies at runtime that the resolved value is a number — if the body came from `JSON.parse`, the promise resolves happily with whatever arrived. The annotation constrains what the compiler lets the body return and what callers may assume; validating untrusted data is a separate job the type layer never does for you.

  • Does returning a promise from an async function produce a nested promise type?
    No. `async function f(): Promise<number> { return Promise.resolve(1); }` is `Promise<number>`, not `Promise<Promise<number>>`. Resolving a promise with another promise adopts its state at runtime, and TypeScript models the same flattening, so the resolved type is unwrapped before the single wrapper is applied.
  • Is a function that returns a promise without the async keyword typed differently?
    No — `function f(): Promise<number> { return Promise.resolve(1); }` has exactly the type an async version would. `async` is an implementation detail enabling `await` in the body; callers see the same signature either way, which is why adding or removing it is not a contract change as long as the resolved type is unchanged.
  • How do you express in the type what an async function rejects with?
    You cannot. The promise type parameterises only the resolved value; rejection types are unrepresentable, and a catch variable is `any`, or `unknown` under `useUnknownInCatchVariables`. If failure modes belong in the contract, model them in the resolved type — a discriminated success/failure object, for instance — rather than relying on thrown errors.

saying these in an interview costs you the question

  • Annotates an async function with the resolved type instead of a promise of it
  • Thinks returning a promise from an async function nests the type
  • Expects the annotation to validate the resolved value at runtime
  • Believes adding the async keyword changes what callers must do
  • Assumes the promise type parameter says something about rejections

context