Node.js 비동기 처리 중 발생한 UnhandledPromiseRejection 디버깅
문제 상황
프로덕션 서버 로그에서 UnhandledPromiseRejectionWarning이 간헐적으로 발생했다. Node.js 8부터는 이 경고가 더 엄격해졌고, 향후 버전에서는 프로세스가 종료될 수 있다는 경고 메시지도 함께 출력됐다.
(node:1234) UnhandledPromiseRejectionWarning: Error: Connection timeout
(node:1234) UnhandledPromiseRejectionWarning: Unhandled promise rejection.
This error originated either by throwing inside of an async function without a catch block,
or by rejecting a promise which was not handled with .catch().
원인 파악
문제가 된 코드는 외부 API 호출 부분이었다.
// 문제 코드
function fetchUserData(userId) {
return axios.get(`/api/users/${userId}`)
.then(response => {
processData(response.data);
return response.data;
});
}
router.get('/users/:id', (req, res) => {
fetchUserData(req.params.id)
.then(data => res.json(data));
});
fetchUserData 함수 내부의 Promise 체인에서 에러 핸들링이 없었고, 라우터에서도 .catch()를 누락했다. API 타임아웃이나 네트워크 에러 발생 시 reject된 Promise가 처리되지 않았다.
해결 방법
모든 Promise 체인에 에러 핸들러를 추가했다.
// 수정 코드
function fetchUserData(userId) {
return axios.get(`/api/users/${userId}`)
.then(response => {
processData(response.data);
return response.data;
})
.catch(error => {
logger.error('Failed to fetch user data', { userId, error: error.message });
throw error;
});
}
router.get('/users/:id', (req, res) => {
fetchUserData(req.params.id)
.then(data => res.json(data))
.catch(error => {
res.status(500).json({ error: 'Internal server error' });
});
});
추가로 프로세스 레벨에서 모니터링을 위한 핸들러도 등록했다.
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
// 알림 발송 로직
});
교훈
Promise를 사용할 때는 항상 .catch()를 체인 끝에 붙이는 습관이 필요하다. 특히 Express 라우터에서는 에러가 자동으로 처리되지 않으므로 더욱 주의해야 한다. async/await 문법을 사용하면 try-catch로 일관되게 처리할 수 있어 이런 실수를 줄일 수 있을 것 같다.