Kodokon kodokon.com

实战中的异步

在你的路由中使用 async/await,并在不拖垮服务器的前提下处理异步错误。

10 分钟 · 3 题

在 Kodokon 中打开本课

你真实的路由不会返回内存里的数组:它们要读取文件、查询数据库、调用其他 API。所有这些操作都是异步的。从 JavaScript 课程起你就认识 async/await 了;我们来看看如何在服务器里干净利落地使用它们。第一个好消息:一个路由函数可以是 async 的。

JAVASCRIPT
import express from "express";
import { readFile } from "node:fs/promises";

const app = express();

app.get("/report", async (req, res) => {
  const raw = await readFile("report.json", "utf8");
  res.json(JSON.parse(raw));
});

app.listen(3000);

这段代码能用……只要 report.json 存在。如果读取失败,promise 会被拒绝,而根据 Express 版本的不同,请求会一直得不到响应,或者错误会泄漏进日志里。专业的规矩是:每个异步路由都要处理自己的错误,用 try/catchnext(error)

JAVASCRIPT
app.get("/report", async (req, res, next) => {
  try {
    const raw = await readFile("report.json", "utf8");
    res.json(JSON.parse(raw));
  } catch (error) {
    next(error);
  }
});

next(error) 会把错误交给集中式错误处理器。它是一个带四个参数的中间件,声明在所有路由之后。一个统一记录和格式化错误的地方:这正是一个可维护的 API 与一个周末玩具项目的区别所在。

JAVASCRIPT
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({
    error: "Internal server error"
  });
});
始终放在最后,位于路由之后

还有最后一个要养成的习惯:并行。两个相互独立、却用两个 await 串起来的操作会一个接一个地运行。Promise.all 让它们一起启动:响应时间等于最慢那个的耗时,而不是两者之和。

JAVASCRIPT
async function fetchUsers() {
  return [{ id: 1, name: "Ada" }];
}

async function fetchTasks() {
  return [{ id: 1, title: "Review the code" }];
}

app.get("/dashboard", async (req, res, next) => {
  try {
    const [users, tasks] = await Promise.all([
      fetchUsers(),
      fetchTasks()
    ]);
    res.json({ users, tasks });
  } catch (error) {
    next(error);
  }
});

知识检测

确认你已牢记本课的重点内容。

  1. 在一个 async 路由里,对于被 catch 捕获的错误,你该怎么做?
    • console.log 记录它,然后继续
    • 调用 next(error),把它交给错误处理器
    • 自动重试这个请求
  2. 两个相互独立的异步调用必须尽可能快地完成。你会选择哪种做法?
    • 用两个连续的 await 把它们串起来
    • Promise.all 让它们一起启动
    • 在两者之间使用 setTimeout
  3. Express 是如何识别一个错误处理中间件的?
    • 靠它那个四参数的签名,其中 err 排在第一位
    • 靠函数名,它必须叫 errorHandler
    • 靠它在文件顶部的位置