基于JavaScript与Mongoose的无效_id错误处理问题排查
问题描述
- 开发模式下:除
updateTour接口外,其他接口能正常返回所有错误信息;调用updateTour时仅返回旧对象,不会触发名称过短等验证错误。 - 生产模式下:所有接口仅返回错误状态码,
updateTour接口同样存在验证错误不触发、无效ID错误处理不当的问题。 - 需求:程序需正确处理无效
_id错误,且updateTour接口能正常触发验证错误并返回对应提示。
相关代码
errorController.js
const AppError = require('./../utils/appError'); const handleCastErrorDB = err => { const message = `Invalid ${err.path}: ${err.value}.`; return new AppError(message, 400); }; const handleDuplicateFieldsDB = err => { const value = err.errmsg.match(/(["'])(\\?.)*?\1/)[0]; console.log(value); const message = `Duplicate field value: ${value}. Please use another value!`; return new AppError(message, 400); }; const handleValidationErrorDB = err => { const errors = Object.values(err.errors).map(el => el.message); const message = `Invalid input data. ${errors.join('. ')}`; return new AppError(message, 400); }; const sendErrorDev = (err, res) => { res.status(err.statusCode).json({ status: err.status, error: err, message: err.message, stack: err.stack }); }; const sendErrorProd = (err, res) => { // Operational, trusted error: send message to client if (err.isOperational) { res.status(err.statusCode).json({ status: err.status, message: err.message }); // Programming or other unknown error: don't leak error details } else { // 1) Log error console.error('ERROR 💥', err); // 2) Send generic message res.status(500).json({ status: 'error', message: 'Something went very wrong!' }); } }; module.exports = (err, req, res, next) => { // console.log(err.stack); err.statusCode = err.statusCode || 500; err.status = err.status || 'error'; if (process.env.NODE_ENV === 'development') { sendErrorDev(err, res); } else if (process.env.NODE_ENV === 'production') { let error = { ...err }; if (error.name === 'CastError') error = handleCastErrorDB(err); if (error.code === 11000) error = handleDuplicateFieldsDB(err); if (error._message === 'Tour validation failed') error = handleValidationErrorDB(err); sendErrorProd(error, res); } };
tourController.js
const Tour = require('../models/tourModel'); const APIFeatures = require('../utils/apiFeatures'); const catchAsync = require('../utils/catchAsync'); const AppError = require('../utils/appError'); /** * Middleware to get the five cheapest tours in the database * @param {*} req * @param {*} res * @param {*} next */ exports.aliasTopTours = (req, res, next) => { req.query.limit = '5'; req.query.sort = '-ratingsAverage,price'; req.query.fields = 'name,price,ratingsAverage,summary,difficulty'; next(); }; /** * Get all tours in the database * @param {*} req * @param {*} res */ exports.getAllTours = catchAsync(async (req, res, next) => { const features = new APIFeatures(Tour.find(), req.query) .filter() .sort() .limitFields() .paginate(); const tours = await features.query; if(!tours){ return next(new AppError('Couldn\'t find any tour in the database',404)) } res.status(200).json({ status: 'success', requestedAt: req.requstTime, results: tours.length, data: { tours, }, }); }); /** * Create a new tour in the database * @param {*} req * @param {*} res */ exports.createTour = catchAsync(async (req, res, next) => { const newTour = await Tour.create(req.body); res.status(201).json({ status: 'success', data: { tour: newTour, }, }); }); /** * Get a single tour in the database * @param {*} req * @param {*} res */ exports.getTour = catchAsync(async (req, res, next) => { const tour = Tour.findById(req.params.id).then((tour) => { res.status(200).json({ status: 'success', requestedAt: req.requstTime, data: { tour, }, }); }).catch((err) => { return next(new AppError('No tour found with that ID', 404)) }); }); /** * Update a single tour in the database * @param {*} req * @param {*} res */ exports.updateTour = catchAsync(async (req, res,next) => { Tour.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true, }).then((tour) => { res.status(200).json({ status: 'success', requestedAt: req.requstTime, data: { tour, }, }); }).catch((err) => { return next(new AppError('No tour found with that ID', 404)) }); }); /** * Delete a single tour in the database */ exports.deleteTour = catchAsync(async (req, res,next ) => { Tour.findByIdAndDelete(req.params.id).then((tour) => { res.status(204).json({ status: 'success', requestedAt: req.requstTime, data: { tour, }, }); }); }); /** * Calculate avg rating and avg price for all tours */ exports.getTourStatus = catchAsync(async (req, res,next) => { const stats = await Tour.aggregate([ { $match: { ratingsAverage: { $gte: 4.5 } }, }, { $group: { _id: { $toUpper: '$difficulty' }, numTours: { $sum: 1 }, numRatings: { $sum: '$ratingsQuantity' }, avgRating: { $avg: '$ratingsAverage' }, avgPrice: { $avg: '$price' }, minPrice: { $min: '$price' }, maxPrice: { $max: '$price' }, }, }, { $sort: { avgPrice: 1 }, }, ]); res.status(200).json({ status: 'success', data: { stats, }, }); }); /** * Show how many tours start in each month * @param {*} req * @param {*} res */ exports.getMonthlyPlan = catchAsync(async (req, res, next) => { const year = req.params.year * 1; const plan = await Tour.aggregate([ { $unwind: '$startDates', }, { $match: { startDates: { $gte: new Date(`2023-01-01`), $lte: new Date(`2023-12-31`), }, }, }, { $group: { _id: { $month: '$startDates' }, numTourStarts: { $sum: 1 }, tours: { $push: '$name' }, }, }, { $addFields: { month: '$_id' }, }, { $project: { _id: 0, }, }, { $sort: { numTourStarts: -1 }, }, ]); res.status(200).json({ status: 'success', results: plan.length, requestedAt: req.requstTime, data: { plan, }, }); });
生产模式运行截图

问题分析与修复方案
核心问题1:混用async/await与Promise链式调用,错误未正确传递到全局错误处理器
updateTour、getTour、deleteTour方法被包裹在catchAsync中,但内部又使用了.then()和.catch()。这种混用会导致:
- 验证错误(如名称过短)、
CastError(无效ID)等数据库错误被本地.catch()捕获,直接返回自定义404错误,无法触发全局错误处理器的格式化逻辑。 catchAsync无法捕获Promise链式调用中的错误,导致错误处理流程断裂。
核心问题2:生产模式下错误对象复制不完整
在errorController.js的生产模式分支中,使用let error = { ...err }复制错误对象,但展开运算符无法复制错误的原型属性(如name、_message),导致后续的错误类型判断失效。
核心问题3:拼写错误导致requestedAt字段异常
代码中多处出现req.requstTime的拼写错误,正确应为req.requestTime(假设你在中间件中设置了该字段)。
修复步骤
1. 统一使用async/await语法,移除.then()和.catch()
以updateTour为例,修改后代码:
exports.updateTour = catchAsync(async (req, res, next) => { const tour = await Tour.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true, }); // 仅当未找到文档时返回404 if (!tour) { return next(new AppError('No tour found with that ID', 404)); } res.status(200).json({ status: 'success', requestedAt: req.requestTime, // 修正拼写错误 data: { tour, }, }); });
同理修改getTour和deleteTour方法,确保错误被catchAsync捕获后传递到全局错误处理器。
2. 修复生产模式下错误对象的复制逻辑
在errorController.js中,替换错误对象复制方式,确保关键属性被保留:
// 原代码 let error = { ...err }; // 修改为 let error = Object.assign({}, err); error.name = err.name; error._message = err._message; error.code = err.code;
3. 验证修复效果
- 开发模式下调用
updateTour传入无效数据(如过短的名称),应返回包含验证错误信息的响应。 - 传入无效
_id时,应返回Invalid _id: xxx的格式化错误信息。 - 生产模式下,所有错误将按
sendErrorProd的逻辑返回对应的友好提示,而非仅状态码。
内容的提问来源于stack exchange,提问作者Tor Bloodaxe
相关产品推荐
相关产品推荐

