Express 미들웨어에서 비동기 에러 핸들링 개선하기
문제 상황
프로젝트에서 Express 라우트 핸들러를 async/await로 전환하면서 모든 핸들러마다 try-catch를 작성해야 했다. 비동기 함수에서 발생한 에러는 자동으로 Express 에러 핸들러로 전달되지 않기 때문이다.
app.get('/users/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id);
res.json(user);
} catch (err) {
next(err); // next를 파라미터로 추가해야 함
}
});
매번 이런 보일러플레이트를 작성하는 게 번거로웠다.
해결 방법
비동기 핸들러를 감싸는 래퍼 함수를 만들었다.
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// 사용
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user);
}));
프로미스 체인에서 발생한 에러를 catch해서 next로 전달한다. 이제 try-catch 없이도 에러가 글로벌 에러 핸들러로 전달된다.
전역 에러 핸들러
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: {
message: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
}
});
});
개발 환경에서는 스택 트레이스를 포함하고, 프로덕션에서는 민감한 정보를 숨긴다.
결과
코드가 간결해졌고 에러 처리 로직이 일관성 있게 유지됐다. express-async-errors 같은 라이브러리도 있지만, 직접 구현한 래퍼가 더 명시적이고 제어 가능해서 만족스러웠다.