As a TypeScript developer with years of experience, I’ve found that promises are one of the most powerful features when dealing with asynchronous operations. Whether you’re fetching data from an API, reading files, or executing time-consuming tasks, promises help you manage the flow of your application.
Recently, I was working on a TypeScript project where I had to fetch some data from an API and then process it before showing it to the user. Initially, I attempted to use basic callbacks, but the code became difficult to manage. As a solution for this issue, I used promises in the TypeScript code to manage the code more efficiently.
In this TypeScript tutorial, I’ll explain how promises work in TypeScript and how we can use them for handling asynchronous tasks, using real-time examples in steps below.
What is a Promise in TypeScript?
A Promise in TypeScript represents a value that may not be available yet but will be resolved at some point in the future. Think of it like ordering food at a restaurant – you place your order (initiate an async operation) and get a receipt (the promise) that guarantees you’ll either get your food (resolved) or a refund if something goes wrong (rejected).
Promises help us handle asynchronous operations more elegantly than callbacks, preventing the infamous “callback hell” that can make code hard to read and maintain.
Creating a Basic Promise in TypeScript
The simplest way to create a promise in TypeScript is by using the Promise constructor:
const myPromise = new Promise<string>((resolve, reject) => {
// Simulate an async operation
setTimeout(() => {
const success = true;
if (success) {
resolve("Operation completed successfully!");
} else {
reject(new Error("Operation failed!"));
}
}, 2000);
});
// Using the promise
myPromise
.then(result => console.log(result))
.catch(error => console.error(error));Output:

In this example, I’ve created a promise that simulates an asynchronous operation taking 2 seconds. The <string> is a TypeScript type annotation indicating that our promise will resolve with a string value.
Promise States
A promise in TypeScript can be in one of three states:
- Pending: Initial state, neither fulfilled nor rejected
- Fulfilled: The operation completed successfully
- Rejected: The operation failed
Once a promise is either fulfilled or rejected, it’s considered “settled” and can’t change states again.
Method 1: Using then(), catch(), and finally()
The most common way to work with promises is using the then(), catch(), and finally() methods:
interface Username {
id: number;
name: string;
email: string;
}
function fetchUserData(userId: number): Promise<Username> {
return fetch(`https://api.example.com/users/${userId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
});
}
// Using the function
fetchUserData(123)
.then(user => {
console.log(`User name: ${user.name}`);
})
.catch(error => {
console.error("Failed to fetch user:", error);
})
.finally(() => {
console.log("Operation finished, regardless of outcome");
});Output:

In this example, I’ve created a function that fetches user data from an API. The then() method handles the successful response, catch() handles any errors, and finally() executes code regardless of whether the promise was fulfilled or rejected.
Check out: Use TypeScript Interface Function Properties
Method 2: Using async/await
While then() and catch() are useful, the async/await syntax makes your code even cleaner and more readable:
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
interface User {
id: number;
name: string;
username: string;
email: string;
}
interface UserProfile extends User {
posts: Post[];
}
// Fetch user profile and posts
async function fetchUserProfile(userId: number): Promise<UserProfile> {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
if (!response.ok) {
throw new Error(`User fetch failed: ${response.status}`);
}
const user: User = await response.json();
const postsResponse = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`);
if (!postsResponse.ok) {
throw new Error(`Posts fetch failed: ${postsResponse.status}`);
}
const posts: Post[] = await postsResponse.json();
return {
...user,
posts,
};
} catch (error) {
console.error("Error fetching user profile:", error);
throw error;
}
}
// Display profile
async function displayUserProfile() {
try {
const profile = await fetchUserProfile(1); // userId 1 exists
console.log(`User: ${profile.name}, Posts: ${profile.posts.length}`);
} catch (error) {
console.error("Failed to display profile:", error);
}
}
// Run the function
displayUserProfile();Output:

With async/await, your asynchronous code looks almost like synchronous code, making it easier to understand the flow. The async keyword before a function makes it return a promise, and await can only be used inside async functions.
Check out: Remove a Property from an Object in TypeScript
Method 3: Promise Combinators
TypeScript offers several methods to work with multiple promises at once:
Promise.all()
Use Promise.all() when you need to wait for multiple promises to complete:
interface User {
id: number;
name: string;
email: string;
}
interface Order {
id: number;
userId: number;
product: string;
quantity: number;
}
interface Notification {
id: number;
userId: number;
message: string;
}
// Mock fetch functions (replace with real APIs if needed)
async function fetchUserData(userId: number): Promise<User> {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
if (!response.ok) throw new Error("Failed to fetch user data");
return response.json();
}
async function fetchUserOrders(userId: number): Promise<Order[]> {
// Using posts API as dummy order data
const response = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`);
if (!response.ok) throw new Error("Failed to fetch orders");
const data = await response.json();
// Map to Order shape (mocked)
return data.map((item: any) => ({
id: item.id,
userId: item.userId,
product: item.title,
quantity: Math.floor(Math.random() * 5) + 1
}));
}
async function fetchUserNotifications(userId: number): Promise<Notification[]> {
// Using comments API as dummy notifications
const response = await fetch(`https://jsonplaceholder.typicode.com/comments?postId=${userId}`);
if (!response.ok) throw new Error("Failed to fetch notifications");
const data = await response.json();
// Map to Notification shape (mocked)
return data.map((item: any) => ({
id: item.id,
userId: userId,
message: item.body
}));
}
// Main function to load dashboard data
async function loadDashboardData(userId: number): Promise<{
user: User;
orders: Order[];
notifications: Notification[];
}> {
try {
const [userData, ordersData, notificationsData] = await Promise.all([
fetchUserData(userId),
fetchUserOrders(userId),
fetchUserNotifications(userId)
]);
return {
user: userData,
orders: ordersData,
notifications: notificationsData
};
} catch (error) {
console.error("Failed to load dashboard:", error);
throw error;
}
}
// Usage
loadDashboardData(1)
.then(data => {
console.log("Dashboard Loaded:");
console.log("User:", data.user.name);
console.log("Orders:", data.orders.length);
console.log("Notifications:", data.notifications.length);
})
.catch(err => {
console.error("Error:", err);
});Output:

Promise.all() waits for all promises to resolve and returns their results as an array. If any promise is rejected, the entire operation fails.
Check out: Mapped Types in TypeScript
Promise.race()
Use Promise.race() when you need only the first promise to settle:
async function fetchWithTimeout<T>(url: string, timeoutMs: number): Promise<T> {
const fetchPromise = fetch(url).then(res => res.json());
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error(`Request timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
return Promise.race([fetchPromise, timeoutPromise]);
}
// Usage
fetchWithTimeout<User>("https://api.example.com/users/123", 5000)
.then(user => console.log(user))
.catch(error => console.error(error));Output:

In this example, I’ve created a function that either resolves with the API response or rejects with a timeout error, whichever comes first.
Check out: TypeScript forEach Loop with Index
Promise.allSettled()
Promise.allSettled() waits for all promises to settle, regardless of whether they fulfill or reject:
async function attemptBatchOperations(operations: Array<() => Promise<any>>) {
console.log(`Starting batch operation with ${operations.length} tasks...\n`);
const promises = operations.map((op, index) =>
op().then(
result => {
console.log(`Operation ${index + 1} succeeded.`);
return result;
},
error => {
console.error(`Operation ${index + 1} failed:`, error);
throw error; // rethrow to mark as rejected
}
)
);
const results = await Promise.allSettled(promises);
const successful = results.filter(r => r.status === 'fulfilled');
const failed = results.filter(r => r.status === 'rejected');
console.log(`\nSummary: ${successful.length} succeeded, ${failed.length} failed`);
return {
successful: successful.map(r => (r as PromiseFulfilledResult<any>).value),
failed: failed.map(r => (r as PromiseRejectedResult).reason)
};
}
// Example usage
const operations = [
() => Promise.resolve("Task 1 complete"),
() => Promise.reject("Task 2 failed"),
() => new Promise(res => setTimeout(() => res("Task 3 complete"), 100)),
() => Promise.reject("Task 4 failed"),
() => new Promise((res) => setTimeout(() => res("Task 5 complete"), 50)),
];
attemptBatchOperations(operations).then(result => {
console.log("\nFinal Result:");
console.log("Successful:", result.successful);
console.log("Failed:", result.failed);
});Output:

This is particularly useful when you want to execute multiple operations and don’t want to abort if some of them fail.
Check out: Loop Through Objects in TypeScript
Handling Promise Errors
Error handling is crucial when working with promises. TypeScript’s type system helps ensure that errors are properly caught and handled:
interface ApiError {
code: string;
message: string;
}
async function fetchData<T>(url: string): Promise<T> {
try {
const response = await fetch(url);
if (!response.ok) {
const errorData: ApiError = await response.json();
throw new Error(`API Error (${errorData.code}): ${errorData.message}`);
}
return response.json();
} catch (error) {
if (error instanceof TypeError) {
// Network error
console.error("Network error:", error.message);
} else {
// Other errors
console.error("Error:", error);
}
throw error;
}
}By typing our error responses and using try/catch blocks appropriately, we can handle different types of errors in a type-safe manner.
Creating Promise Utilities
Over the years, I’ve found it helpful to create utility functions for common promise patterns:
// Retry a promise-returning function a specific number of times
async function retry<T>(
fn: () => Promise<T>,
retries: number,
delay: number = 300
): Promise<T> {
try {
return await fn();
} catch (error) {
if (retries <= 0) throw error;
await new Promise(resolve => setTimeout(resolve, delay));
return retry(fn, retries - 1, delay * 1.5); // Exponential backoff
}
}
// Usage
const userData = await retry(
() => fetchUserData(123),
3 // Retry up to 3 times
);Output:

This utility function will retry a failed promise operation with exponential backoff, which is perfect for handling transient API failures.
I hope you found this guide to TypeScript promises helpful. Promises are a powerful tool in your TypeScript code, and mastering them will help you write cleaner, more maintainable asynchronous code.

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.