Back to all posts

Writing Clean Asynchronous Code

S
sonhp
•
10 min read

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

S

Written by sonhp

Technical writer and developer passionate about web technologies.

Related Articles