Express 미들웨어 에러 핸들링 제대로 하기
문제 상황
재택근무로 전환되면서 API 서버 모니터링이 더 중요해졌다. 그런데 Sentry에 찍혀야 할 에러들이 누락되는 경우가 있었다.
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user);
});
위 코드에서 User.findById가 실패하면 UnhandledPromiseRejection이 발생한다. Express의 기본 에러 핸들러가 이를 잡지 못한다.
해결 방법
1. async wrapper 유틸리티
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.get('/api/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user);
}));
2. 중앙 에러 핸들러
app.use((err, req, res, next) => {
console.error(err.stack);
// Sentry 전송
Sentry.captureException(err);
// 환경별 응답 분기
const isDev = process.env.NODE_ENV === 'development';
res.status(err.status || 500).json({
error: isDev ? err.message : 'Internal Server Error',
...(isDev && { stack: err.stack })
});
});
3. 커스텀 에러 클래스
class ApiError extends Error {
constructor(message, status = 500) {
super(message);
this.status = status;
}
}
app.get('/api/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw new ApiError('User not found', 404);
}
res.json(user);
}));
결과
- 누락되던 에러들이 Sentry에 제대로 기록됨
- 에러 응답 형식이 통일됨
- 디버깅 시간 단축
express-async-errors 같은 라이브러리도 있지만, 직접 구현한 wrapper가 더 명시적이고 통제 가능해서 이 방식을 선택했다.