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.
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.
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:
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.
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.
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.
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.
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.
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.
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
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.
Frequently Asked Questions
What does an async function return in TypeScript?
Does await work outside of async functions in TypeScript?
How does try/catch work with async/await in TypeScript?
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.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.