Node.js 비동기 에러 핸들링 - 놓치기 쉬운 Promise rejection
문제 상황
프로덕션 서버에서 간헐적으로 API 요청이 응답 없이 끊기는 현상이 발생했다. 로그를 확인해보니 특정 Promise가 reject되었을 때 에러를 catch하지 않아 unhandledRejection이 발생하고 있었다.
app.get('/api/users/:id', async (req, res) => {
const user = await getUserFromDB(req.params.id); // 여기서 에러 발생 가능
res.json(user);
});
문제는 getUserFromDB가 실패했을 때 에러가 Express 미들웨어로 전달되지 않는다는 것이었다.
해결 방법
1. try-catch로 감싸기
app.get('/api/users/:id', async (req, res, next) => {
try {
const user = await getUserFromDB(req.params.id);
res.json(user);
} catch (error) {
next(error);
}
});
2. asyncHandler 유틸리티 작성
매번 try-catch를 작성하는 것이 번거로워 래퍼 함수를 만들었다.
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 getUserFromDB(req.params.id);
res.json(user);
}));
3. 전역 unhandledRejection 리스너 추가
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// 프로덕션에서는 로깅 후 프로세스 종료 고려
});
교훈
Express는 기본적으로 async/await 에러를 자동으로 처리하지 않는다. Node.js 10부터는 unhandledRejection이 발생하면 경고를 출력하고, 향후 버전에서는 프로세스를 종료할 예정이라고 한다. 모든 async 라우트 핸들러에 에러 처리를 추가했고, PM2로 프로세스가 죽었을 때 자동 재시작되도록 설정했다.