Back to patterns

Promise Chaining

WORKFLOW

Visualize and debug complex asynchronous flows and promise chains for structured reasoning and step-by-step task completion.

Diagram for Promise Chaining

Use Cases

  • Complex data fetching workflows
  • Multi-step form submissions
  • Dependent API calls
  • Resource cleanup chains

Production Implementation

Implementation
// Real-world example: User checkout flow with inventory check, payment, and order creation
interface InventoryCheck {
  productId: string;
  available: boolean;
  quantity: number;
}

interface PaymentResult {
  transactionId: string;
  status: 'success' | 'failed';
  amount: number;
}

interface Order {
  orderId: string;
  userId: string;
  items: Array<{ productId: string; quantity: number }>;
  status: 'pending' | 'confirmed' | 'failed';
}

class CheckoutService {
  private logger = console; // Replace with your logging service

  async processCheckout(
    userId: string,
    items: Array<{ productId: string; quantity: number }>
  ): Promise<Order> {
    try {
      // Step 1: Check inventory for all items
      const inventoryChecks = await this.checkInventory(items);
      const unavailableItems = inventoryChecks.filter(check => !check.available);
      
      if (unavailableItems.length > 0) {
        throw new Error(`Items out of stock: ${unavailableItems.map(item => item.productId).join(', ')}`);
      }

      // Step 2: Calculate total and process payment
      const total = await this.calculateTotal(items);
      const paymentResult = await this.processPayment(userId, total);

      if (paymentResult.status === 'failed') {
        throw new Error(`Payment failed for transaction ${paymentResult.transactionId}`);
      }

      // Step 3: Create order
      const order = await this.createOrder(userId, items, paymentResult);
      
      // Step 4: Update inventory (should be transactional in production)
      await this.updateInventory(items);

      this.logger.info('Checkout completed successfully', {
        orderId: order.orderId,
        userId,
        items: items.length,
        total: paymentResult.amount
      });

      return order;

    } catch (error) {
      this.logger.error('Checkout process failed', {
        userId,
        items: items.length,
        error: error instanceof Error ? error.message : 'Unknown error'
      });

      // Attempt cleanup/rollback if needed
      await this.handleCheckoutError(error, userId, items);
      
      throw error;
    }
  }

  private async checkInventory(
    items: Array<{ productId: string; quantity: number }>
  ): Promise<InventoryCheck[]> {
    try {
      // Parallel inventory checks for all items
      return await Promise.all(
        items.map(async item => {
          const inventory = await this.inventoryAPI.check(item.productId);
          return {
            productId: item.productId,
            available: inventory.quantity >= item.quantity,
            quantity: inventory.quantity
          };
        })
      );
    } catch (error) {
      this.logger.error('Inventory check failed', { items, error });
      throw new Error('Failed to verify inventory availability');
    }
  }

  private async calculateTotal(
    items: Array<{ productId: string; quantity: number }>
  ): Promise<number> {
    try {
      const prices = await Promise.all(
        items.map(item => this.pricingAPI.get(item.productId))
      );

      return items.reduce((total, item, index) => {
        return total + (prices[index] * item.quantity);
      }, 0);
    } catch (error) {
      this.logger.error('Price calculation failed', { items, error });
      throw new Error('Failed to calculate order total');
    }
  }

  private async processPayment(
    userId: string,
    amount: number
  ): Promise<PaymentResult> {
    try {
      const paymentResult = await this.paymentAPI.charge({
        userId,
        amount,
        currency: 'USD'
      });

      this.logger.info('Payment processed', {
        userId,
        amount,
        transactionId: paymentResult.transactionId
      });

      return paymentResult;
    } catch (error) {
      this.logger.error('Payment processing failed', {
        userId,
        amount,
        error
      });
      throw new Error('Payment processing failed');
    }
  }

  private async createOrder(
    userId: string,
    items: Array<{ productId: string; quantity: number }>,
    payment: PaymentResult
  ): Promise<Order> {
    try {
      const order = await this.orderAPI.create({
        userId,
        items,
        paymentId: payment.transactionId,
        status: 'confirmed'
      });

      this.logger.info('Order created', {
        orderId: order.orderId,
        userId,
        items: items.length
      });

      return order;
    } catch (error) {
      this.logger.error('Order creation failed', {
        userId,
        items,
        paymentId: payment.transactionId,
        error
      });
      throw new Error('Failed to create order');
    }
  }

  private async updateInventory(
    items: Array<{ productId: string; quantity: number }>
  ): Promise<void> {
    try {
      await Promise.all(
        items.map(item =>
          this.inventoryAPI.decrease(item.productId, item.quantity)
        )
      );
    } catch (error) {
      this.logger.error('Inventory update failed', { items, error });
      throw new Error('Failed to update inventory');
    }
  }

  private async handleCheckoutError(
    error: unknown,
    userId: string,
    items: Array<{ productId: string; quantity: number }>
  ): Promise<void> {
    this.logger.warn('Initiating checkout error cleanup', {
      userId,
      items: items.length,
      error
    });

    try {
      // Implement your cleanup/rollback logic here
      // For example:
      // - Refund payment if it was processed
      // - Restore inventory if it was decreased
      // - Update order status if it was created
    } catch (cleanupError) {
      this.logger.error('Cleanup after checkout failure failed', {
        originalError: error,
        cleanupError
      });
    }
  }
}

// Usage Example
async function handleUserCheckout(userId: string, cartItems: CartItem[]) {
  const checkoutService = new CheckoutService();
  
  try {
    const order = await checkoutService.processCheckout(userId, cartItems);
    
    // Handle successful checkout
    notifyUser(userId, {
      type: 'checkout_success',
      orderId: order.orderId
    });
    
  } catch (error) {
    // Handle checkout failure
    notifyUser(userId, {
      type: 'checkout_failed',
      error: error instanceof Error ? error.message : 'Checkout failed'
    });
    
    // Redirect to error page or show error message
    throw error;
  }
}

Code Examples

Common Promise Chain Issues

typescript
// ❌ Common Issues in Promise Chains
async function processOrder(orderId: string) {
  // Issue 1: No error handling
  const order = await fetchOrder(orderId);
  const user = await fetchUser(order.userId);
  await processPayment(order.amount);
  
  // Issue 2: Sequential requests that could be parallel
  const items = [];
  for (const itemId of order.itemIds) {
    const item = await fetchItem(itemId);
    items.push(item);
  }
  
  // Issue 3: No cleanup on failure
  await updateInventory(items);
  await sendConfirmation(user.email);
}

Common issues include lack of error handling, inefficient sequential requests, and missing cleanup logic.

Improved Promise Chain

typescript
// ✅ Better Promise Chain Implementation
async function processOrder(orderId: string) {
  let inventoryUpdated = false;
  
  try {
    // Fetch order and user in parallel
    const [order, user] = await Promise.all([
      fetchOrder(orderId),
      fetchUser(order.userId)
    ]);

    // Validate order status
    if (order.status !== 'pending') {
      throw new Error(`Invalid order status: ${order.status}`);
    }

    // Process payment with retry logic
    const payment = await retry(
      () => processPayment(order.amount),
      { maxAttempts: 3 }
    );

    // Fetch items in parallel
    const items = await Promise.all(
      order.itemIds.map(fetchItem)
    );

    // Update inventory with transaction
    await beginTransaction();
    await updateInventory(items);
    inventoryUpdated = true;
    await commitTransaction();

    // Send confirmation
    await sendConfirmation(user.email);

    logger.info('Order processed successfully', {
      orderId,
      userId: user.id,
      amount: order.amount
    });

  } catch (error) {
    logger.error('Order processing failed', {
      orderId,
      error: error instanceof Error ? error.message : 'Unknown error'
    });

    // Cleanup: Rollback inventory if needed
    if (inventoryUpdated) {
      try {
        await rollbackInventory(items);
      } catch (rollbackError) {
        logger.error('Inventory rollback failed', {
          orderId,
          error: rollbackError
        });
      }
    }

    throw error;
  }
}

This improved version includes parallel requests, proper error handling, cleanup logic, and logging.

Best Practices

  • Use Promise.all() for parallel operations when requests are independent
  • Implement proper error handling with specific error types
  • Add logging for debugging and monitoring
  • Include cleanup/rollback logic for failures
  • Use transactions for related operations
  • Implement retry logic for transient failures
  • Add proper TypeScript types for better maintainability

Common Pitfalls

  • Running requests sequentially when they could be parallel
  • Missing error handling or using generic catch-all handlers
  • Not implementing cleanup logic for partial failures
  • Forgetting to log important events and errors
  • Not handling edge cases in the business logic
  • Missing type definitions leading to runtime errors