阿里云主机折上折
  • 微信号
Current Site:Index > The async/await syntactic sugar

The async/await syntactic sugar

Author:Chuan Chen 阅读数:51153人阅读 分类: Node.js

The Essence of async/await

async/await is syntactic sugar for Generator functions, implemented based on Promises under the hood. It makes asynchronous code look like synchronous code, but the actual execution remains asynchronous. An async function returns a Promise object, and the expression following the await command is typically a Promise object.

async function fetchData() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  return data;
}

Basic Usage of async Functions

Adding the async keyword before a function declaration turns it into an async function. Inside an async function, you can use await expressions, which pause the execution of the async function until the Promise is resolved before continuing.

async function getUser(id) {
  try {
    const response = await fetch(`/users/${id}`);
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return await response.json();
  } catch (error) {
    console.error('Fetch error:', error);
  }
}

Behavior of await Expressions

The await expression pauses the execution of the async function and waits for the Promise to resolve. If the awaited value is not a Promise, it is returned directly. await can only be used inside async functions.

async function process() {
  const value1 = await 42; // Non-Promise values are returned directly
  const value2 = await Promise.resolve('hello');
  console.log(value1, value2); // 42 "hello"
}

Error Handling Mechanism

Inside async functions, you can use try/catch to handle errors or use the Promise's catch method. Uncaught errors will cause the returned Promise to be rejected.

// Using try/catch
async function fetchWithRetry(url, retries = 3) {
  try {
    const response = await fetch(url);
    return await response.json();
  } catch (err) {
    if (retries <= 0) throw err;
    return fetchWithRetry(url, retries - 1);
  }
}

// Using the catch method
fetchWithRetry('https://api.example.com/data')
  .then(data => console.log(data))
  .catch(err => console.error('Failed after retries:', err));

Parallel Execution of Multiple Asynchronous Operations

When multiple independent asynchronous operations need to be executed in parallel, use Promise.all with await instead of sequentially awaiting each operation.

async function fetchMultiple() {
  const [user, posts] = await Promise.all([
    fetch('/user/1').then(r => r.json()),
    fetch('/posts?userId=1').then(r => r.json())
  ]);
  return { user, posts };
}

Execution Order of async Functions

Understanding the execution order of async functions is important. await pauses function execution but does not block the event loop.

console.log('Start');

async function asyncFunc() {
  console.log('Async function start');
  await Promise.resolve();
  console.log('Async function after await');
}

asyncFunc();

console.log('End');

// Output order:
// Start
// Async function start
// End
// Async function after await

Using await in Loops

When using await in loops, pay attention to the execution order. Sometimes parallel execution is needed instead of sequential execution.

// Sequential execution
async function processArray(array) {
  for (const item of array) {
    await processItem(item); // Each item is processed sequentially
  }
}

// Parallel execution
async function processArrayParallel(array) {
  await Promise.all(array.map(item => processItem(item)));
}

Interoperability Between async Functions and Promises

Async functions return Promises, so they can seamlessly work with other Promise APIs.

async function getUser(id) {
  return { id, name: `User ${id}` };
}

// Can be used like a regular Promise
getUser(1)
  .then(user => console.log(user))
  .catch(err => console.error(err));

// Can also be used inside another async function
async function printUser(id) {
  const user = await getUser(id);
  console.log(user);
}

Common Use Cases

async/await is particularly suitable for handling asynchronous operations that need to be executed sequentially, such as API call chains and file processing.

async function processOrder(orderId) {
  const order = await fetchOrder(orderId);
  const user = await fetchUser(order.userId);
  const address = await fetchAddress(user.addressId);
  await sendConfirmationEmail(user.email, order, address);
  return { order, user, address };
}

Performance Considerations

Although async/await makes code more readable, improper use can impact performance. Avoid unnecessary await and parallelize operations appropriately.

// Inefficient approach
async function inefficient() {
  const a = await fetchA();
  const b = await fetchB(); // Waits for fetchA to complete before starting
  return a + b;
}

// Efficient approach
async function efficient() {
  const [a, b] = await Promise.all([fetchA(), fetchB()]);
  return a + b;
}

Usage in Class Methods

Async functions can also be used as class methods, maintaining the same syntax and behavior.

class ApiClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }

  async get(resource) {
    const response = await fetch(`${this.baseUrl}/${resource}`);
    return response.json();
  }
}

const client = new ApiClient('https://api.example.com');
client.get('users/1').then(user => console.log(user));

Comparison with Generator Functions

async/await can be seen as syntactic sugar for Generator functions but is more focused on asynchronous flow control.

// Using Generator functions
function* generatorFetch() {
  const response = yield fetch('/data');
  const data = yield response.json();
  return data;
}

// Using async/await
async function asyncFetch() {
  const response = await fetch('/data');
  const data = await response.json();
  return data;
}

Practical Applications in Node.js

In Node.js, async/await is commonly used for I/O-intensive tasks like file operations and database queries.

const fs = require('fs').promises;

async function readFiles() {
  try {
    const file1 = await fs.readFile('file1.txt', 'utf8');
    const file2 = await fs.readFile('file2.txt', 'utf8');
    return { file1, file2 };
  } catch (err) {
    console.error('Error reading files:', err);
    throw err;
  }
}

Integration with Event Emitters

Event emitters can be wrapped into Promises for use in async functions.

const { EventEmitter } = require('events');

function eventToPromise(emitter, eventName) {
  return new Promise((resolve, reject) => {
    emitter.once(eventName, resolve);
    emitter.once('error', reject);
  });
}

async function waitForEvent() {
  const emitter = new EventEmitter();
  setTimeout(() => emitter.emit('data', 'Hello'), 1000);
  const data = await eventToPromise(emitter, 'data');
  console.log(data); // 'Hello'
}

Usage in Express Routes

Using async/await in Express route handlers simplifies error handling.

const express = require('express');
const app = express();

app.get('/user/:id', async (req, res, next) => {
  try {
    const user = await getUserById(req.params.id);
    if (!user) return res.status(404).send('User not found');
    res.json(user);
  } catch (err) {
    next(err); // Errors are passed to the error-handling middleware
  }
});

async function getUserById(id) {
  // Simulate a database query
  return new Promise(resolve => {
    setTimeout(() => resolve({ id, name: `User ${id}` }), 100);
  });
}

Integration with Stream Processing

Although stream processing is typically event-based, helper functions can be created to make it compatible with async/await.

const { pipeline } = require('stream/promises');
const fs = require('fs');

async function processFile(inputFile, outputFile) {
  try {
    await pipeline(
      fs.createReadStream(inputFile),
      // Transformation streams can be added here
      fs.createWriteStream(outputFile)
    );
    console.log('Pipeline succeeded');
  } catch (err) {
    console.error('Pipeline failed:', err);
  }
}

Usage in Testing

async/await simplifies asynchronous test code, making test cases clearer.

const assert = require('assert');
const { test } = require('node:test');

test('async test example', async () => {
  const result = await someAsyncFunction();
  assert.strictEqual(result, expectedValue);
});

async function someAsyncFunction() {
  return new Promise(resolve => setTimeout(() => resolve(42), 10));
}

Debugging async Functions

Debugging async functions is similar to debugging synchronous code, but pay attention to the call stack and breakpoint locations.

async function debugExample() {
  console.log('Start debugging');
  const result = await fetchData(); // A breakpoint can be set here
  console.log('Data:', result);
  const processed = process(result); // Another breakpoint location
  return processed;
}

Browser Compatibility and Transpilation

Modern browsers support async/await, but older environments may require Babel transpilation.

// Babel will transpile this into compatible code
async function oldBrowserSupport() {
  const data = await fetchData();
  console.log(data);
}

Interaction with Web Workers

Using async/await in Web Workers simplifies message handling.

// worker.js
self.onmessage = async (event) => {
  try {
    const result = await heavyComputation(event.data);
    self.postMessage({ status: 'success', result });
  } catch (err) {
    self.postMessage({ status: 'error', error: err.message });
  }
};

async function heavyComputation(data) {
  // Simulate heavy computation
  return new Promise(resolve => {
    setTimeout(() => resolve(data * 2), 1000);
  });
}

Usage in Timers

Timer functions like setTimeout can be Promisified for use in async functions.

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function timedOperation() {
  console.log('Start');
  await delay(1000);
  console.log('After 1 second');
  await delay(500);
  console.log('After another 0.5 second');
}

Integration with Third-Party Libraries

Many modern Node.js libraries support Promises and can seamlessly integrate with async/await.

const axios = require('axios');

async function fetchFromMultipleAPIs() {
  const [github, reddit] = await Promise.all([
    axios.get('https://api.github.com/users/octocat'),
    axios.get('https://www.reddit.com/r/node.json')
  ]);
  return { github: github.data, reddit: reddit.data };
}

Recursive async Functions

Async functions can call themselves recursively, but be mindful of stack usage and performance.

async function recursiveFetch(url, depth = 0) {
  if (depth > 3) return null;
  const response = await fetch(url);
  const data = await response.json();
  if (data.next) {
    return recursiveFetch(data.next, depth + 1);
  }
  return data;
}

Applications in CLI Tools

In Node.js CLI tools, async/await simplifies asynchronous operations like user input handling.

const readline = require('readline').createInterface({
  input: process.stdin,
  output: process.stdout
});

function questionAsync(prompt) {
  return new Promise(resolve => {
    readline.question(prompt, resolve);
  });
}

async function interactiveCLI() {
  const name = await questionAsync('What is your name? ');
  const age = await questionAsync('How old are you? ');
  console.log(`Hello ${name}, you are ${age} years old`);
  readline.close();
}

Memory Management Considerations

Long-running async functions may hold unnecessary references, so be mindful of memory leaks.

async function processLargeData() {
  const bigData = await getLargeData(); // Large dataset
  const result = computeResult(bigData);
  // Explicitly clear references to large data that's no longer needed
  bigData = null;
  return result;
}

Interaction with WebSockets

WebSocket communication can be wrapped in Promises for use in async functions.

function websocketPromise(ws, message) {
  return new Promise((resolve, reject) => {
    ws.send(message);
    ws.once('message', resolve);
    ws.once('error', reject);
    ws.once('close', () => reject(new Error('WebSocket closed')));
  });
}

async function wsCommunication(ws) {
  try {
    const response = await websocketPromise(ws, 'ping');
    console.log('Received:', response);
  } catch (err) {
    console.error('WebSocket error:', err);
  }
}

Applications in Microservice Architectures

When making calls between microservices, async/await simplifies cross-service communication code.

async function placeOrder(userId, items) {
  const user = await userService.getUser(userId);
  const inventory = await inventoryService.checkItems(items);
  const payment = await paymentService.processPayment(user, items);
  const order = await orderService.createOrder(user, items, payment);
  await notificationService.sendOrderConfirmation(user, order);
  return order;
}

Integration with GraphQL Resolvers

GraphQL resolvers are well-suited for async/await as they often need to fetch data from multiple sources.

const resolvers = {
  Query: {
    user: async (_, { id }) => {
      const user = await db.users.findById(id);
      const posts = await db.posts.findByUserId(id);
      return { ...user, posts };
    }
  },
  Mutation: {
    createPost: async (_, { input }, context) => {
      if (!context.user) throw new Error('Unauthorized');
      return db.posts.create({ ...input, authorId: context.user.id });
    }
  }
};

Usage in Serverless Functions

Serverless functions often need to handle various asynchronous operations, making async/await an ideal choice.

// AWS Lambda example
exports.handler = async (event) => {
  try {
    const data = await processEvent(event);
    return {
      statusCode: 200,
      body: JSON.stringify(data)
    };
  } catch (err) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: err.message })
    };
  }
};

async function processEvent(event) {
  // Process event data
  return { processed: true, timestamp: Date.now() };
}

Integration with TypeScript's Type System

TypeScript provides excellent type support for async functions, allowing precise description of return types.

interface User {
  id: number;
  name: string;
}

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/users/${id}`);
  return response.json();
}

async function printUserName(id: number): Promise<void> {
  const user = await fetchUser(id);
  console.log(user.name);
}

Applications in Middleware Pipelines

Middleware pipelines can be designed as chains of async functions for easy request and response handling.

async function middlewarePipeline(context, middlewares) {
  for (const middleware of middlewares) {
    await middleware(context);
    if (context.finished) break;
  }
  return context;
}

// Usage example
const context = { request: {}, response: {} };
await middlewarePipeline(context, [
  async (ctx) => { ctx.user = await getUser(ctx.request); },
  async (ctx) => { if (!ctx.user) throw new Error('Unauthorized'); },
  async (ctx) => { ctx.response.data = await getData(ctx.user); }
]);

Integration with Database Transactions

Database transaction processing is a classic use case for async/await, requiring sequential execution of multiple operations.

async function transferFunds(senderId, receiverId, amount) {
  const connection = await db.getConnection();
  try {
    await connection.beginTransaction();
    
    await connection.query(
      'UPDATE accounts SET balance = balance - ? WHERE id = ?',
      [amount, senderId]
    );
    
    await connection.query(
      'UPDATE accounts SET balance = balance + ? WHERE id = ?',
      [amount, receiverId]
    );
    
    await connection.commit();
    return true;
  } catch (err) {
    await connection.rollback();
    throw err;
  } finally {
    connection.release();
  }
}

Handling Paginated Data

async/await simplifies the process of fetching paginated data, allowing sequential retrieval of all pages.

async function fetchAllPages(baseUrl) {
  let allData = [];
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const response = await fetch(`${baseUrl}?page=${page}`);
    const { data, totalPages } = await response.json();
    allData = allData.concat(data);
    hasMore = page < totalPages;
    page++;
  }

  return allData;
}

Implementing Retry Logic

async/await makes implementing retry logic simple and intuitive.

async function fetchWithRetry(url, options = {}, retries = 3) {
  try {
    const response = await fetch(url, options);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.json();
  } catch (err) {
    if (retries <= 0) throw err;
    await new Promise(r => setTimeout(r, 1000 * (4 - retries))); // Exponential backoff
    return fetchWithRetry(url, options, retries - 1);
  }
}

Integration with Caching Strategies

Async functions can easily implement caching logic to reduce unnecessary duplicate requests.

const cache = new Map();

async function getWithCache(key, fetcher) {
  if (cache.has(key)) {
    return cache.get(key);
  }
  const data = await fetcher();
  cache.set(key, data);
  return data;
}

async function getUserWithCache(id) {
  return getWithCache(`user:${id}`, () => fetchUser(id));
}

Handling Concurrency Limits

Concurrency control can be implemented via async functions to limit the number of simultaneous asynchronous operations.

class TaskQueue {
  constructor(concurrency) {
    this.concurrency = concurrency;
    this.running = 0;
    this.queue = [];
  }

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

上一篇:Promise原理与使用

下一篇:错误处理策略

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.