翻译此页面

错误处理

错误处理是指 Express 如何捕获并处理同步代码与异步代码中发生的错误。 Express 自带默认错误处理器,因此你在开发初期无需自行编写。

错误捕获

确保 Express 能够捕获路由处理器和中间件运行时发生的所有错误,这一点至关重要。

路由处理函数与中间件内同步代码产生的错误无需额外处理。 倘若同步代码抛出错误,Express 会捕获并处理该错误。 举个例子:

app.get('/', (req, res) => {
throw new Error('BROKEN'); // Express will catch this on its own.
});

对于路由处理函数和中间件调用的异步函数所返回的错误,你必须将它们传递给 next() 函数,Express 会在该函数中捕获并处理这些错误。 举个例子:

app.get('/', (req, res, next) => {
fs.readFile('/file-does-not-exist', (err, data) => {
if (err) {
next(err); // Pass errors to Express.
} else {
res.send(data);
}
});
});

从 Express 5 开始,返回 Promise 的路由处理程序和中间件在拒绝(reject)或抛出错误时,将自动调用 next(value)。 举个例子:

app.get('/user/:id', async (req, res, next) => {
const user = await getUserById(req.params.id);
res.send(user);
});

如果 getUserById 抛出错误或被拒绝(reject),next 将会使用抛出的错误或被拒绝的值来调用。 如果未提供拒绝值,则会使用 Express 路由内置的默认错误对象调用 next

如果向 next() 传入任意参数(字符串 'route' 除外),Express 会将当前请求视作出错,并跳过后续所有非错误处理的路由与中间件函数。

如果序列中的回调函数不返回数据、仅返回错误,你可以将代码简化如下:

app.get('/', [
function (req, res, next) {
fs.writeFile('/inaccessible-path', 'data', next);
},
function (req, res) {
res.send('OK');
},
]);

在上述示例中,next 被作为 fs.writeFile 的回调函数传入,无论是否存在错误都会被调用。 若无错误,则执行第二个处理函数;否则 Express 会捕获并处理该错误。

你必须捕获在路由处理程序或中间件中调用的异步代码里发生的错误,并将它们传递给 Express 进行处理。 举个例子:

app.get('/', (req, res, next) => {
setTimeout(() => {
try {
throw new Error('BROKEN');
} catch (err) {
next(err);
}
}, 100);
});

上述示例使用 try...catch 代码块捕获异步代码中的错误,并将其传递给 Express。 如果省略 try...catch 代码块,Express 将无法捕获该错误,因为它不属于同步处理程序代码的一部分。

使用 Promise 来避免 try...catch 代码块的开销,或在使用返回 Promise 的函数时采用此方式。 举个例子:

app.get('/', (req, res, next) => {
Promise.resolve()
.then(() => {
throw new Error('BROKEN');
})
.catch(next); // Errors will be passed to Express.
});

由于 Promise 会自动捕获同步错误和被拒绝的 Promise,你只需将 next 作为最终的 catch 处理程序传入即可,Express 会捕获错误,因为 catch 处理程序会将错误作为第一个参数传入。

你也可以使用处理程序链,将异步代码简化为简单逻辑,从而依靠同步错误捕获机制。 举个例子:

app.get('/', [
function (req, res, next) {
fs.readFile('/maybe-valid-file', 'utf-8', (err, data) => {
res.locals.data = data;
next(err);
});
},
function (req, res) {
res.locals.data = res.locals.data.split(',')[1];
res.send(res.locals.data);
},
]);

上述示例包含几条来自 readFile 调用的简单语句。 如果 readFile 引发错误,则会将错误传递给 Express;否则你将迅速回到处理程序链中下一个处理函数的同步错误处理流程。 然后,上述示例会尝试处理数据。 如果此过程失败,同步错误处理程序将会捕获该错误。 如果你将此处理逻辑写在 readFile 回调函数内部,应用程序可能会直接退出,Express 错误处理程序将无法执行。

无论你使用哪种方法,若希望 Express 错误处理程序被调用且应用程序持续运行,必须确保 Express 能够接收到错误。

默认错误处理程序

Express 内置了错误处理程序,可处理应用中可能出现的所有错误。 这个默认的错误处理中间件函数会被添加到中间件函数栈的末尾。

如果你将错误传递给 next(),但未在自定义错误处理程序中处理它,该错误将由内置错误处理程序处理;错误信息和堆栈追踪信息会被返回给客户端。 生产环境中不会包含堆栈追踪信息。

Note

将环境变量 NODE_ENV 设置为 production,即可在生产模式下运行应用。

当错误信息被输出时,响应中会附加以下信息:

  • res.statusCode 会从 err.status(或 err.statusCode)中获取设置。 如果该值不在 4xx 或 5xx 范围内,则会被设置为 500。
  • res.statusMessage 会根据状态码自动设置。
  • 生产环境中,响应主体会展示状态码信息的 HTML 页面;其他环境下则会展示 err.stack 堆栈信息。
  • err.headers 对象中定义的所有响应头。

如果在开始向客户端写入响应数据后调用带错误参数的next()(例如向客户端流式返回响应时出错),Express 默认错误处理程序会关闭连接并终止本次请求。

因此,当你添加自定义错误处理程序时,若响应头已发送至客户端,必须将错误委托给 Express 默认错误处理程序处理:

function errorHandler(err, req, res, next) {
if (res.headersSent) {
return next(err);
}
res.status(500);
res.render('error', { error: err });
}

注意:即便已配置自定义错误处理中间件,若代码中多次传入错误调用next(),仍可能触发默认错误处理程序。

Other error handling middleware can be found at Express middleware.

编写错误处理程序

错误处理中间件函数的定义方式与其他中间件函数基本相同,区别在于错误处理函数包含四个参数而非三个: (err, req, res, next)。 举个例子:

app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});

你需要在其他 app.use() 调用和路由之后,最后定义错误处理中间件;例如:

index.cjs
const bodyParser = require('body-parser');
const methodOverride = require('method-override');
app.use(
bodyParser.urlencoded({
extended: true,
})
);
app.use(bodyParser.json());
app.use(methodOverride());
app.use((err, req, res, next) => {
// logic
});

中间件函数内的响应可以是任意格式,例如 HTML 错误页面、简单消息或 JSON 字符串。

出于组织结构(以及高级框架)的目的,你可以定义多个错误处理中间件函数,这与你使用常规中间件函数的方式非常相似。 例如,为使用 XHR 发起的请求和未使用 XHR 发起的请求分别定义错误处理程序:

index.cjs
const bodyParser = require('body-parser');
const methodOverride = require('method-override');
app.use(
bodyParser.urlencoded({
extended: true,
})
);
app.use(bodyParser.json());
app.use(methodOverride());
app.use(logErrors);
app.use(clientErrorHandler);
app.use(errorHandler);

In this example, the generic logErrors might write request and error information to stderr, for example:

function logErrors(err, req, res, next) {
console.error(err.stack);
next(err);
}

Also in this example, clientErrorHandler is defined as follows; in this case, the error is explicitly passed along to the next one.

Notice that when not calling “next” in an error-handling function, you are responsible for writing (and ending) the response. Otherwise, those requests will “hang” and will not be eligible for garbage collection.

function clientErrorHandler(err, req, res, next) {
if (req.xhr) {
res.status(500).send({ error: 'Something failed!' });
} else {
next(err);
}
}

Implement the “catch-all” errorHandler function as follows (for example):

function errorHandler(err, req, res, next) {
res.status(500);
res.render('error', { error: err });
}

If you have a route handler with multiple callback functions, you can use the route parameter to skip to the next route handler. 举个例子:

app.get(
'/a_route_behind_paywall',
(req, res, next) => {
if (!req.user.hasPaid) {
// continue handling this request
next('route');
} else {
next();
}
},
(req, res, next) => {
PaidContent.find((err, doc) => {
if (err) return next(err);
res.json(doc);
});
}
);

In this example, the getPaidContent handler will be skipped but any remaining handlers in app for /a_route_behind_paywall would continue to be executed.

Note

Calls to next() and next(err) indicate that the current handler is complete and in what state. next(err) will skip all remaining handlers in the chain except for those that are set up to handle errors as described above.