TypeScript Promises
A Promise is an object that represents the eventual result of an asynchronous operation. When the operation finishes, the Promise either resolves with a value (success) or rejects with an error (failure). TypeScript types Promises with a generic parameter — Promise<T> — so the resolved value always has a known type, eliminating guesswork in async code.
What a Promise Looks Like in Real Life
REAL WORLD:
You order food online
│
├── Promise created: "Your food will arrive"
│
├── PENDING → kitchen is cooking (in progress)
│
├── FULFILLED → food delivered! (resolved with value)
│
└── REJECTED → restaurant cancelled (rejected with reason)
IN CODE:
let order = new Promise((resolve, reject) => {
// async work happens here
});
Promise States
| State | Meaning | What Happens Next |
|---|---|---|
| Pending | Operation is running | Moves to fulfilled or rejected |
| Fulfilled | Operation succeeded | .then() callback runs with the value |
| Rejected | Operation failed | .catch() callback runs with the error |
Creating a Typed Promise
// Promise<string> means the resolved value will be a string
function fetchWelcomeMessage(userId: number): Promise<string> {
return new Promise((resolve, reject) => {
if (userId <= 0) {
reject(new Error("Invalid user ID"));
return;
}
// Simulate network delay
setTimeout(() => {
resolve(`Welcome back, User ${userId}!`);
}, 1000);
});
}
fetchWelcomeMessage(5)
.then((message: string) => {
console.log(message); // Welcome back, User 5! (after 1 sec)
})
.catch((error: Error) => {
console.log("Error:", error.message);
});
Promise Chaining with Types
Each .then() can transform the value and pass it to the next .then(). TypeScript tracks the type at each stage.
CHAIN FLOW:
fetchId() → Promise<number>
.then(fetchUser) → Promise<User>
.then(formatUser) → Promise<string>
.catch(handleErr) → catches any error above
function fetchId(): Promise<number> {
return Promise.resolve(42);
}
function fetchUser(id: number): Promise<{ name: string; email: string }> {
return Promise.resolve({ name: "Divya", email: "divya@mail.com" });
}
function formatUser(user: { name: string; email: string }): string {
return `${user.name} (${user.email})`;
}
fetchId()
.then(fetchUser)
.then(formatUser)
.then((formatted: string) => {
console.log(formatted); // Divya (divya@mail.com)
})
.catch((error: Error) => {
console.log("Pipeline failed:", error.message);
});
Promise.resolve and Promise.reject
// Create an immediately resolved promise
let success: Promise<number> = Promise.resolve(100);
success.then(n => console.log(n)); // 100
// Create an immediately rejected promise
let failure: Promise<never> = Promise.reject(new Error("Immediate failure"));
failure.catch(e => console.log(e.message)); // Immediate failure
Promise.all — Wait for All
DIAGRAM:
Promise A ────────────── resolve(userA)
Promise B ──── resolve(userB)
Promise C ─────────────────── resolve(userC)
│
all() resolves
with [userA, userB, userC]
interface CourseInfo {
title: string;
students: number;
}
async function loadCourses(): Promise<void> {
let courseIds = [1, 2, 3];
let coursePromises: Promise<CourseInfo>[] = courseIds.map(id =>
fetchCourse(id)
);
let courses: CourseInfo[] = await Promise.all(coursePromises);
courses.forEach(c => console.log(`${c.title}: ${c.students} students`));
}
function fetchCourse(id: number): Promise<CourseInfo> {
let courses: CourseInfo[] = [
{ title: "TypeScript", students: 1200 },
{ title: "React", students: 3500 },
{ title: "Node.js", students: 2100 }
];
return Promise.resolve(courses[id - 1]);
}
Promise.race — First One Wins
// Resolves or rejects as soon as the FIRST promise settles
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
let timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
async function main(): Promise<void> {
try {
let result = await withTimeout(fetchUser(1), 2000);
console.log("Got result in time:", result);
} catch (error: unknown) {
if (error instanceof Error) {
console.log(error.message); // Timed out after 2000ms
}
}
}
Typed Promise Utility
// Generic retry function — retries a failing promise up to N times
async function retry<T>(
fn: () => Promise<T>,
attempts: number
): Promise<T> {
let lastError: Error = new Error("Unknown error");
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (error: unknown) {
if (error instanceof Error) {
lastError = error;
console.log(`Attempt ${i + 1} failed: ${error.message}`);
}
}
}
throw lastError;
}
let callCount = 0;
async function unstableApi(): Promise<string> {
callCount++;
if (callCount < 3) throw new Error("Server overloaded");
return "Data loaded successfully";
}
retry(unstableApi, 5).then(result => {
console.log(result); // Data loaded successfully (on 3rd attempt)
});
Promises vs async/await
| Aspect | Promises (.then/.catch) | async/await |
|---|---|---|
| Readability | Gets complex with chaining | Reads like synchronous code |
| Error handling | .catch() at end | try/catch blocks |
| Parallel execution | Promise.all() | await Promise.all() |
| Race condition | Promise.race() | await Promise.race() |
| Under the hood | Foundation | Syntax sugar over Promises |
Both work together. async/await is built on Promises. Knowing how Promises work helps write better async/await code and debug it effectively.
