Promise chaining helps you write cleaner TypeScript code by handling multiple asynchronous operations in sequence. Let’s explore how to use promise chaining effectively, with practical examples and tips for better code organization.
Table of Contents
- What is Promise Chaining?
- Basic Promise Chain Structure
- Error Handling Made Simple
- TypeScript’s Type Safety Benefits
- Running Promises in Parallel
- Tips for Better Promise Chains
- Using Async/Await
- Working with Callback-Based Code
What is Promise Chaining?
When you need to run several asynchronous tasks one after another, promise chaining lets you write clear, manageable code. Instead of nesting callbacks (which leads to messy code), you can chain promises using the .then()
method.
Basic Promise Chain Structure
type User = {
id: number;
name: string;
};
type UserDetails = {
email: string;
phone: string;
};
function fetchUser(id: number): Promise<User> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id,
name: 'John Doe'
});
}, 1000);
});
}
function fetchUserDetails(user: User): Promise<UserDetails> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
email: `${user.name.toLowerCase()}@example.com`,
phone: '123-456-7890'
});
}, 1000);
});
}
// Example of promise chaining
fetchUser(1)
.then(user => fetchUserDetails(user))
.then(details => console.log(details))
.catch(error => console.error('Error:', error));
Code language: JavaScript (javascript)
Error Handling Made Simple
One great thing about promise chains is how they handle errors. You can catch errors from any step using a single .catch()
at the end:
function validateUser(user: User): Promise<User> {
return new Promise((resolve, reject) => {
if (user.name.length > 0) {
resolve(user);
} else {
reject(new Error('Invalid user name'));
}
});
}
fetchUser(1)
.then(user => validateUser(user))
.then(user => fetchUserDetails(user))
.then(details => console.log(details))
.catch(error => console.error('Error in chain:', error.message));
Code language: JavaScript (javascript)
TypeScript’s Type Safety Benefits
TypeScript makes promise chains safer by checking types throughout the chain:
interface ProcessedUser extends User {
processed: boolean;
}
function processUser(user: User): Promise<ProcessedUser> {
return new Promise((resolve) => {
resolve({
...user,
processed: true
});
});
}
// TypeScript ensures type safety
fetchUser(1)
.then(user => processUser(user))
.then(processedUser => {
// TypeScript knows processedUser has the 'processed' property
console.log(processedUser.processed);
return processedUser;
})
.catch(error => console.error(error));
Code language: JavaScript (javascript)
Running Promises in Parallel
Sometimes you’ll want to run multiple promises at once while keeping them in a chain. Here’s how to use Promise.all()
:
function fetchUserPreferences(userId: number): Promise<object> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ theme: 'dark', language: 'en' });
}, 1000);
});
}
fetchUser(1)
.then(user => {
return Promise.all([
fetchUserDetails(user),
fetchUserPreferences(user.id)
]);
})
.then(([details, preferences]) => {
console.log('User details:', details);
console.log('User preferences:', preferences);
})
.catch(error => console.error(error));
Code language: JavaScript (javascript)
Tips for Better Promise Chains
Keep Your Chains Flat
Don’t nest promise chains – it makes code harder to read:
// Bad approach - nested chains
fetchUser(1)
.then(user => {
fetchUserDetails(user)
.then(details => {
console.log(details);
});
});
// Better approach - flat chain
fetchUser(1)
.then(user => fetchUserDetails(user))
.then(details => console.log(details));
Code language: JavaScript (javascript)
Always Return Values
Make sure to return values in each .then()
block:
fetchUser(1)
.then(user => {
const processed = { ...user, lastAccess: new Date() };
return processed; // Don't forget to return!
})
.then(processedUser => {
console.log(processedUser);
});
Code language: JavaScript (javascript)
Add Type Annotations
Clear type annotations make code easier to maintain:
type UserResponse = {
user: User;
details: UserDetails;
};
fetchUser(1)
.then((user: User) => fetchUserDetails(user))
.then((details: UserDetails): UserResponse => {
return {
user: { id: 1, name: 'John Doe' },
details
};
})
.then((response: UserResponse) => {
console.log(response);
});
Code language: JavaScript (javascript)
Using Async/Await
While promise chains work well, async/await often makes code even clearer:
async function getUserData(id: number): Promise<UserResponse> {
try {
const user = await fetchUser(id);
const details = await fetchUserDetails(user);
return { user, details };
} catch (error) {
console.error('Error fetching user data:', error);
throw error;
}
}
Code language: JavaScript (javascript)
Working with Callback-Based Code
Here’s how to turn old callback functions into promises:
function legacyFunction(callback: (error: Error | null, result?: string) => void) {
setTimeout(() => {
callback(null, 'Result');
}, 1000);
}
function promisified(): Promise<string> {
return new Promise((resolve, reject) => {
legacyFunction((error, result) => {
if (error) {
reject(error);
} else {
resolve(result!);
}
});
});
}
// Now you can use it in a chain
promisified()
.then(result => console.log(result))
.catch(error => console.error(error));
Code language: JavaScript (javascript)
Promise chaining makes async code more manageable in TypeScript. By following these patterns, you’ll write cleaner code that’s easier to understand and maintain. For more complex async patterns, check out our guide on TypeScript Async/Await: Complete Guide to Modern Asynchronous Code.