Node.js 비동기 에러 핸들링 - Promise catch 놓쳐서 서버 죽은 이야기
문제 상황
새벽 3시에 모니터링 알림이 울렸다. API 서버가 응답하지 않는다는 내용이었다. 로그를 확인해보니 특정 요청 이후 프로세스가 종료된 것으로 보였다.
문제가 된 코드는 이랬다:
app.get('/api/users/:id', (req, res) => {
fetchUserFromDB(req.params.id)
.then(user => res.json(user));
});
fetchUserFromDB에서 DB 연결 오류가 발생했을 때 .catch()를 붙이지 않아서 unhandled promise rejection이 발생했고, Node.js 프로세스가 종료되었다.
해결 방법
1. 개별 Promise에 catch 추가
app.get('/api/users/:id', (req, res) => {
fetchUserFromDB(req.params.id)
.then(user => res.json(user))
.catch(err => {
console.error('DB Error:', err);
res.status(500).json({ error: 'Internal Server Error' });
});
});
2. 전역 unhandledRejection 핸들러
모든 Promise에 catch를 붙이는 것은 실수하기 쉽다. 최소한 프로세스가 죽지 않도록 전역 핸들러를 추가했다.
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// 로깅은 하되 프로세스는 유지
});
3. Express 에러 미들웨어 활용
동기/비동기 에러를 통합 처리하기 위해 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 fetchUserFromDB(req.params.id);
res.json(user);
}));
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
});
정리
Node.js 8에서는 unhandled rejection이 경고만 출력하지만, 향후 버전에서는 프로세스를 종료할 예정이라고 한다. Promise를 사용할 때는 반드시 에러 처리를 해야 한다는 교훈을 얻었다.