正在加载,请稍候…

Async Patterns in Node.js: Promises, async/await, and Concurrency

Master async programming in Node.js. Learn Promise patterns, async/await best practices, concurrency control, error handling, and avoiding common pitfalls.

Async Patterns in Node.js: Promises, async/await, and Concurrency

Promise Fundamentals

// Creating Promises
const delay = (ms: number): Promise<void> =>
  new Promise(resolve => setTimeout(resolve, ms));

const fetchUser = (id: string): Promise<User> =>
  new Promise((resolve, reject) => {
    db.find(id, (err, user) => {
      if (err) reject(err);
      else if (!user) reject(new Error('Not found'));
      else resolve(user);
    });
  });

Parallel vs Sequential Execution

// Sequential (slow - each waits for previous)
const user1 = await fetchUser('1');
const user2 = await fetchUser('2');
const user3 = await fetchUser('3');

// Parallel (fast - all run concurrently)
const [user1, user2, user3] = await Promise.all([
  fetchUser('1'),
  fetchUser('2'),
  fetchUser('3'),
]);

// First to resolve wins
const firstAvailable = await Promise.race([
  fetchFromPrimary(id),
  delay(500).then(() => fetchFromFallback(id)),
]);

// All settle (don't fail on individual errors)
const results = await Promise.allSettled([
  fetchUser('1'),
  fetchUser('invalid'),
  fetchUser('3'),
]);

results.forEach(result => {
  if (result.status === 'fulfilled') console.log(result.value);
  else console.error(result.reason);
});

Concurrency Control

// Process array with limited concurrency (p-limit pattern)
async function mapWithConcurrency<T, R>(
  items: T[],
  fn: (item: T) => Promise<R>,
  concurrency: number
): Promise<R[]> {
  const results: R[] = [];
  const queue = [...items];
  
  const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
    while (queue.length > 0) {
      const item = queue.shift()!;
      const result = await fn(item);
      results.push(result);
    }
  });

  await Promise.all(workers);
  return results;
}

// Usage: process 100 users but only 5 at a time
const processed = await mapWithConcurrency(userIds, processUser, 5);

Error Handling Patterns

// Wrapping with Result type
async function safeAsync<T>(
  fn: () => Promise<T>
): Promise<{ data: T; error: null } | { data: null; error: Error }> {
  try {
    return { data: await fn(), error: null };
  } catch (err) {
    return { data: null, error: err as Error };
  }
}

const { data: user, error } = await safeAsync(() => fetchUser(id));
if (error) {
  console.error('Failed to fetch user:', error.message);
  return;
}
// TypeScript knows user is not null here

// Retry logic
async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 1000
): Promise<T> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === maxRetries) throw err;
      await delay(baseDelay * Math.pow(2, attempt));
    }
  }
  throw new Error('unreachable');
}

Async Iteration

// Process database results as stream
async function* streamUsers(filter: UserFilter) {
  let page = 0;
  const pageSize = 100;

  while (true) {
    const users = await userRepo.findAll({ ...filter, page, pageSize });
    if (users.length === 0) break;
    yield* users;
    page++;
  }
}

for await (const user of streamUsers({ role: 'admin' })) {
  await processUser(user);
}

AbortController for Cancellation

async function fetchWithTimeout<T>(
  fn: (signal: AbortSignal) => Promise<T>,
  timeoutMs: number
): Promise<T> {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    return await fn(controller.signal);
  } finally {
    clearTimeout(timeoutId);
  }
}

// Usage
const user = await fetchWithTimeout(
  (signal) => fetch('/api/user', { signal }).then(r => r.json()),
  5000
);

Common Anti-Patterns

// Bad: Unnecessary await in map creates sequential execution
const users = await Promise.all(ids.map(async id => {
  return await fetchUser(id);  // 'await' inside async map is fine but redundant
}));

// Bad: Forgetting to await
async function updateAllUsers(updates: UserUpdate[]): Promise<void> {
  updates.forEach(async update => {  // Bug! forEach doesn't await Promises
    await processUpdate(update);
  });
}

// Good: Use for...of or Promise.all
async function updateAllUsers(updates: UserUpdate[]): Promise<void> {
  for (const update of updates) {
    await processUpdate(update);
  }
  // or: await Promise.all(updates.map(processUpdate));
}

Understanding the Node.js event loop and async patterns is fundamental to writing performant server code.