Async and Await in TypeScript

Learn how async and await work with TypeScript's type system. Covers async function return types, typed await expressions, and error handling with try/catch.

6 min read

Async and await are syntax that makes working with promises feel like writing synchronous code. In TypeScript, they work with the same type system as then chains, but the type flow is more direct: you write await before a promise, and TypeScript gives you the resolved value with its correct type already applied.

An async function always returns a promise. TypeScript figures out the resolved type from your return statement, so you get type checking on both what the function produces and how callers use it.

How Async Functions Return Promises

Mark a function with the async keyword, and TypeScript automatically wraps its return type in a promise. You can annotate the return type explicitly or let inference handle it.

typescripttypescript
async function getValue(): Promise<number> {
  return 42;
}

The annotation Promise of number tells TypeScript and anyone reading the code what this function eventually produces. If you later change the implementation to return a string, the compiler catches the mismatch.

You can also let TypeScript infer the return type. It reads the return statement and wraps the type automatically:

typescripttypescript
async function getValue() {
  return 42;
}

TypeScript infers Promise of number because 42 is a number. If you return nothing, the return type becomes Promise of void, meaning the caller knows no useful value is produced.

The rule is consistent: whatever type your return statement produces, TypeScript wraps it in Promise. If you return a promise directly, TypeScript does not double-wrap it. A return of Promise of number inside an async function still produces Promise of number.

How await Unwraps Types

The await keyword takes a promise and gives you the resolved value with its type extracted. Each await strips one layer of Promise.

typescripttypescript
async function fetchData() {
  const response = await fetch("/api/data");
  const data = await response.json();
  return data;
}

The first await waits for the fetch promise to settle and gives you a Response object. The second await waits for the JSON parsing promise and gives you the parsed data. TypeScript tracks the type through every await step, so each variable has the correct type at each point.

Async function type flow with await

The diagram shows how await resolves each promise to its inner type, and how the final return is automatically wrapped back into a promise. The full chain is typed from start to finish without a single manual type annotation.

Awaiting Non-Promise Values

You can await any value, not just promises. If the value is not a promise, it passes through unchanged with its exact type preserved.

typescripttypescript
async function example() {
  const a = await 42;
  const b = await "hello";
  const c = await { x: 1, y: 2 };
}

The variable a is typed as number, b as string, and c as the object type. TypeScript does not wrap non-promise values. This is useful when a function may return either a value or a promise, and you want to handle both uniformly with a single await.

Error Handling with try/catch

In an async function, rejected promises become exceptions that you can catch with try/catch. With strict mode enabled, TypeScript types the caught variable as unknown, unlike the reason parameter in a promise's .catch() method, which stays any.

typescripttypescript
async function loadConfig() {
  try {
    const response = await fetch("/config.json");
    const config = await response.json();
    return config;
  } catch (error) {
    if (error instanceof Error) {
      console.log("Failed to load config:", error.message);
    }
    return null;
  }
}

Without the instanceof check, accessing error.message produces a compiler error, since strict mode types the caught variable as unknown. TypeScript forces you to verify the error type because any value can be thrown in JavaScript, not just Error objects.

For more on structured error handling, see custom error classes in TypeScript.

Returning from Multiple Code Paths

An async function must return a consistent type from every code path. TypeScript checks all branches.

typescripttypescript
async function fetchWithFallback(url: string): Promise<string> {
  try {
    const response = await fetch(url);
    return await response.text();
  } catch {
    return "fallback value";
  }
}

Both the try block and the catch block return a string, so the overall return type is Promise of string. If the catch block returned a number, TypeScript would report an error because the declared return type expects a string.

Using Promise.all with await

You can combine Promise.all with await to run multiple promises concurrently and get typed results in a tuple.

typescripttypescript
async function loadPage() {
  const [user, posts, settings] = await Promise.all([
    fetchUser(1),
    fetchPosts(1),
    fetchSettings(),
  ]);
}

TypeScript preserves each position's type. The variable user gets the User type, posts gets an array of posts, and settings gets the Settings type. No manual casting is needed because Promise.all's return type is inferred from the input array.

Common Mistakes

Forgetting that async functions return promises. Calling an async function without await or then gives you a promise, not the value. Use await to extract the resolved value, or use then if you need to chain further operations.

Not handling the error type in catch. The error in catch is unknown, not Error. Always narrow with instanceof before accessing properties.

Looping with sequential awaits when Promise.all would work. If you have independent async calls, use Promise.all instead of awaiting each one in a loop. It is faster and the type inference works identically.

For more on typing the resolved values from promises, see type promise return values.

Rune AI

Rune AI

Key Insights

  • An async function always returns a promise, wrapping its return type automatically.
  • await unwraps a promise to its resolved value, tracked at compile time.
  • You can await any value. Non-promise values pass through unchanged.
  • try/catch errors are typed as unknown in strict mode, stricter than a promise's .catch(). Narrow before using.
  • Top-level await is available with ES2022+ module settings.
RunePowered by Rune AI

Frequently Asked Questions

What does an async function return in TypeScript?

An async function always returns a Promise<T>, where T is the type of the value you return. If you return a string, the function returns Promise<string>. If you return nothing, it returns Promise<void>. TypeScript enforces this automatically.

Does await work outside of async functions in TypeScript?

It depends on your module setting. In ES2022 or ESNext modules, top-level await is allowed. In CommonJS or older module formats, await is only valid inside async functions. TypeScript respects your tsconfig module setting.

How does try/catch work with async/await in TypeScript?

The caught error is typed as unknown in strict mode. This is stricter than a promise's .catch() method, where the reason parameter stays any. Either way, narrow the error type with instanceof or a type guard before accessing properties like .message.

Conclusion

Async and await make TypeScript's promise typing feel natural. An async function returns a promise of its return type automatically, and await unwraps that promise to the value. The compiler checks every awaited value against how you use it, and try/catch types its caught variable as unknown in strict mode so you narrow before using it. The result is async code that reads like synchronous code while keeping full type safety.