Asynchronous Functions and Promises: A TypeScript Guide
Learn to type async functions and Promises in TypeScript. Master API response handling with generic types to ensure your network layer remains type-safe.

Previously in this course, we covered defining function return types. While that taught us how to handle synchronous data, modern web development relies heavily on non-blocking operations. In this lesson, we'll extend those skills to master async functions and Promises.
Understanding the Promise Wrapper
In JavaScript, an async function always returns a Promise. A Promise is an object representing the eventual completion (or failure) of an asynchronous operation. In TypeScript, we represent this using the generic Promise<T> type, where T is the type of the value the promise eventually resolves to.
If you have a function that fetches a user's name as a string, its return type isn't just string—it’s Promise<string>.
Annotating Async Functions
When you mark a function with the async keyword, TypeScript requires the return type to be wrapped in a Promise. If you omit the explicit return type, TypeScript’s inference engine will usually calculate it for you, but explicit annotations are a best practice for API clients where you want to enforce a contract.
Consider this example of a basic task fetcher:
TYPESCRIPTinterface Task { id: number; title: string; completed: boolean; } // Correctly typing the async function return async function fetchTask(id: number): Promise<Task> { const response = await fetch(CE9178">`/api/tasks/${id}`); const data: Task = await response.json(); return data; }
By explicitly setting : Promise<Task>, you guarantee that anyone calling fetchTask knows they must await the result or use .then().
Handling API Response Wrappers
Real-world APIs rarely return raw objects. They often wrap responses in metadata. Typing these wrappers is the first step in building a robust API client.
Imagine your task API returns a structure like this: { data: Task, status: number }. You should define an interface for the response wrapper and use it in your function signature.
TYPESCRIPTinterface ApiResponse<T> { data: T; status: number; } async function fetchTaskWrapped(id: number): Promise<ApiResponse<Task>> { const response = await fetch(CE9178">`/api/tasks/${id}`); // We cast or validate the response to match our wrapper return await response.json(); }
The <T> here is a generic type parameter. It allows us to reuse ApiResponse for different data shapes (like a list of tasks or a single user profile) while keeping the wrapper logic consistent.
Hands-on Exercise: Typing the Task Client
In our running project, let's create a function to fetch all tasks.
- Define an interface
Taskwithidandtitle. - Create an
asyncfunction namedgetTasksthat returnsPromise<Task[]>. - Inside, use a mock fetch (or
Promise.resolve) to return an array of tasks.
TYPESCRIPT// Your code here async function getTasks(): Promise<Task[]> { // Simulate API call return Promise.resolve([{ id: 1, title: "Finish course" }]); }
Common Pitfalls
- Forgetting the Promise wrapper: A common mistake is annotating an async function as returning
Taskinstead ofPromise<Task>. TypeScript will throw an error because the function actually returns a promise object. - Implicit
anyinresponse.json(): The.json()method in the Fetch API returnsanyby default. Always assign the result to a typed variable or cast it to your interface to ensure type safety. - Ignoring Rejection: Remember that
asyncfunctions can throw. While we are focusing on return types, keep in mind that your call site needs to handle potential errors, as discussed in Node.js Async Error Handling in Express: A Practical Guide.
FAQ
Why does my async function return Promise<void>?
If your function does not explicitly return a value (e.g., it just performs a console.log or a side-effecting API call), TypeScript correctly infers or expects Promise<void>.
Can I use async/await without Promises?
No. async functions are syntactic sugar for Promises. Every time you mark a function as async, you are opting into the Promise-based architecture.
How do I handle errors in these functions?
You should return the expected type on success and throw an error on failure. The caller is responsible for using try/catch blocks to handle the rejection.
Recap
We've learned that async functions are typed by wrapping their resolution value in a Promise<T>. By using generic interfaces like ApiResponse<T>, we can maintain strict type safety even when APIs return complex, wrapped data structures. This is a critical foundation for the robust API client we are building throughout this course.
Up next: We will explore how to use Union Types for Flexibility to handle different task statuses in our client.
Work with me

Next.js Full-Stack Web App Development
A fast, SEO-ready full-stack web app built with Next.js 16 — from idea to deployed product, by an engineer who ships to production.

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.
