Writing Clean and Maintainable Code in NestJS
Part 8 of 12
Writing Clean Asynchronous Code
Asynchronous programming is essential in modern applications, but if not handled properly, it can lead to nested promises, callback hell, and unhandled exceptions.
In this guide, we’ll cover:
- Why clean asynchronous code matters
- Common async pitfalls
- Using async/await properly
- Handling concurrent operations
- Error handling in async functions
- Best practices for NestJS async services
1. Why Writing Clean Async Code Matters
When working with database queries, APIs, or background jobs, async operations can quickly become messy.
Bad async code leads to:
❌ Deeply nested callbacks (callback hell)
❌ Unreadable promise chains
❌ Unhandled promise rejections
❌ Race conditions in concurrent tasks
A well-structured async codebase is:
✅ Easier to debug
✅ More readable & maintainable
✅ More performant
2. Common Mistakes in Asynchronous Code
❌ Callback Hell
getUser(id, (user) => {
getOrders(user.id, (orders) => {
processOrders(orders, (result) => {
console.log('Done:', result);
});
});
});
🚨 Problem: Deeply nested callbacks make code unreadable.
✅ Solution: Use async/await.
❌ Chaining Promises Poorly
this.userService
.findUser(id)
.then((user) => {
return this.orderService.getOrders(user.id);
})
.then((orders) => {
return this.paymentService.processPayments(orders);
})
.catch((error) => {
console.error(error);
});
🚨 Problem: Hard to read and maintain.
✅ Solution: Use async/await.
❌ Forgetting to Handle Async Errors
async function getUserData(id: string) {
const user = await this.userService.findUser(id);
const orders = await this.orderService.getOrders(user.id);
return orders;
}
🚨 Problem: If
findUser
fails, the function crashes without handling the error.
✅ Solution: Use try/catch blocks.
3. Using Async/Await Properly
✅ Refactored Code with Async/Await
async function getUserOrders(userId: string) {
try {
const user = await this.userService.findUser(userId);
if (!user) throw new NotFoundException('User not found');
const orders = await this.orderService.getOrders(user.id);
return orders;
} catch (error) {
throw new InternalServerErrorException(error.message);
}
}
✅ Easier to read
✅ Proper error handling
✅ No nested promises
4. Handling Concurrent Async Operations
❌ Sequential Requests (Slow)
async function getUserData(userId: string) {
const user = await this.userService.findUser(userId);
const orders = await this.orderService.getOrders(user.id);
return { user, orders };
}
🚨 Problem: Requests execute one after another, slowing down performance.
✅ Using Promise.all
for Parallel Execution
async function getUserData(userId: string) {
const [user, orders] = await Promise.all([
this.userService.findUser(userId),
this.orderService.getOrders(userId),
]);
return { user, orders };
}
🚀 Faster execution! Both requests run at the same time.
✅ Improves performance
✅ Reduces API/database calls time
5. Handling Errors in Async Functions
❌ Forgetting await
in Try/Catch
async function fetchData() {
try {
return this.httpService.get('https://api.example.com/data');
} catch (error) {
console.error('Failed to fetch data');
}
}
🚨 Problem: The function doesn’t
await
the request, so errors won’t be caught.
✅ Fix: Await Inside Try/Catch
async function fetchData() {
try {
const response = await this.httpService.get('https://api.example.com/data');
return response.data;
} catch (error) {
console.error('Failed to fetch data:', error);
throw new InternalServerErrorException('Data fetch failed');
}
}
✅ Now, errors are properly handled.
6. Best Practices for Async Code in NestJS
✅ Use async/await
over callbacks and promise chains
✅ Handle errors with try/catch, not silently fail
✅ Use Promise.all
for parallel tasks
✅ Always await
async functions to catch errors properly
✅ Wrap async handlers in NestJS Exception Filters
7. Summary: Writing Clean Async Code
Key Takeaways
✅ Avoid callback hell – use async/await
✅ Write clean, readable async functions
✅ Handle errors with try/catch
✅ Optimize performance with Promise.all