# 异步与错误处理

URL: https://caijiao.org/express/chapter6/4-async-error
Source: docs/express/chapter6/4-async-error.md
Description: 本章节介绍如何在 Express 和 Mongoose 中处理异步操作及错误，确保应用在高并发情况下的稳定性和可靠性。

在开发现代的 Express 应用时，异步编程是必不可少的。尤其是涉及数据库操作时，Mongoose 提供了强大的异步支持，包括通过 `async/await` 和回调函数来处理数据库请求。本章节将介绍如何在 Express 和 Mongoose 中正确处理异步操作及错误，确保应用的高可用性和稳定性。

## 异步操作

在 Express 中，大多数操作（如请求处理、数据库交互等）都是异步的。异步操作常常涉及回调函数或 `Promise`，而 `async/await` 是处理异步操作的现代方式。

### 1. 使用 `async/await` 处理异步操作

`async/await` 提供了一种更直观的方式来处理异步操作，可以让你像同步代码一样编写异步代码。以下是一个使用 `async/await` 处理数据库查询的示例：

```js
// 使用 async/await 查询用户
async function getUser(req, res) {
  try {
    const user = await User.findOne({ username: req.params.username });
    if (!user) {
      return res.status(404).send('用户未找到');
    }
    res.json(user);
  } catch (err) {
    console.error('查询失败:', err);
    res.status(500).send('服务器错误');
  }
}
````

### 2. 在 Express 路由中使用 `async/await`

在 Express 路由中，我们可以使用 `async/await` 来简化代码并处理异步任务。以下是一个完整的示例：

```js
app.get('/user/:username', async (req, res) => {
  try {
    const user = await User.findOne({ username: req.params.username });
    if (!user) {
      return res.status(404).send('用户未找到');
    }
    res.json(user);
  } catch (err) {
    console.error('数据库查询失败:', err);
    res.status(500).send('服务器错误');
  }
});
```

### 3. 使用 `Promise` 进行异步操作

`Promise` 是 [JavaScript](/javascript/) 中处理异步操作的基础，可以链式调用 `then` 和 `catch` 来处理结果和错误。虽然 `async/await` 更直观，但在某些场景下，`Promise` 依然有其优势。

```js
User.findOne({ username: req.params.username })
  .then((user) => {
    if (!user) {
      return res.status(404).send('用户未找到');
    }
    res.json(user);
  })
  .catch((err) => {
    console.error('查询失败:', err);
    res.status(500).send('服务器错误');
  });
```

## 错误处理

在开发应用时，错误处理是一个重要的方面。良好的错误处理可以帮助我们捕获问题并确保应用稳定运行。

### 1. 使用 `try/catch` 捕获错误

在异步函数中，`try/catch` 用于捕获可能发生的异常。如果异步操作失败，错误会被捕获并处理。

```js
async function updateUser(req, res) {
  try {
    const user = await User.findById(req.params.id);
    if (!user) {
      return res.status(404).send('用户未找到');
    }
    user.name = req.body.name;
    await user.save();
    res.json(user);
  } catch (err) {
    console.error('更新失败:', err);
    res.status(500).send('服务器错误');
  }
}
```

### 2. Express 中间件处理错误

Express 提供了一个错误处理中间件来集中处理所有路由中的错误。你可以定义一个全局的错误处理器来捕获未处理的错误。

```js
// 错误处理中间件
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('服务器错误');
});
```

此中间件需要放在所有路由和其他中间件之后，才能捕获整个应用中的错误。

### 3. 自定义错误处理类

为了增强错误处理的可读性和一致性，我们可以创建一个自定义的错误处理类。该类可以将不同类型的错误（如验证错误、数据库错误等）封装成统一的格式。

```js
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
    this.isOperational = true;

    Error.captureStackTrace(this, this.constructor);
  }
}

module.exports = AppError;
```

使用自定义错误类的示例：

```js
const AppError = require('./AppError');

async function getUser(req, res, next) {
  try {
    const user = await User.findOne({ username: req.params.username });
    if (!user) {
      return next(new AppError('用户未找到', 404));
    }
    res.json(user);
  } catch (err) {
    next(err); // 将错误传递给错误处理中间件
  }
}
```

### 4. 错误响应格式化

为了保持一致的错误响应格式，可以将所有错误信息返回给客户端时格式化为统一结构。通常，错误信息会包含 `status`、`message` 和其他相关的错误细节。

```js
// 错误处理中间件
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  const status = err.status || 'error';
  const message = err.message || '服务器错误';
  res.status(statusCode).json({
    status,
    message,
  });
});
```

### 5. 捕获未处理的异常

为了防止未处理的异常崩溃应用，通常可以在应用启动时添加 `process.on` 事件监听，捕获未处理的 promise rejection 和异常。

```js
// 捕获未处理的 promise rejection
process.on('unhandledRejection', (err) => {
  console.error('未处理的 promise rejection:', err);
  // 这里可以选择关闭应用或进行其他处理
});

// 捕获未处理的异常
process.on('uncaughtException', (err) => {
  console.error('未处理的异常:', err);
  // 可以选择退出应用
});
```

## 异常日志记录

在生产环境中，捕获并记录错误日志是一个重要的环节。通过记录日志，我们可以更轻松地调试和排查错误。

### 1. 使用日志库

可以使用流行的日志库，如 `winston` 或 `morgan` 来记录应用中的错误信息和请求日志。

```bash
npm install winston
```

配置 `winston` 日志记录器：

```js
const winston = require('winston');

const logger = winston.createLogger({
  transports: [
    new winston.transports.Console({ format: winston.format.simple() }),
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
  ],
});

module.exports = logger;
```

在错误处理中间件中使用 `winston` 记录错误：

```js
const logger = require('./logger');

app.use((err, req, res, next) => {
  logger.error(err.stack);
  res.status(500).send('服务器错误');
});
```

## 总结

* **异步操作**：在 Express 中，使用 `async/await` 处理异步任务能够让代码更简洁、易读，并且避免回调地狱。
* **错误处理**：通过 `try/catch` 捕获异常，使用全局错误处理中间件来处理未捕获的错误。
* **自定义错误**：可以使用自定义错误类来统一管理和处理错误。
* **日志记录**：通过日志库记录错误信息，有助于排查和调试问题。
* **全局异常捕获**：通过 `process.on` 捕获未处理的异常和 promise rejection，确保应用稳定运行。

通过良好的异步处理和错误管理，我们可以确保 Express 应用在生产环境中的可靠性和稳定性。
