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로 일관되게 처리할 수 있어 이런 실수를 줄일 수 있을 것 같다.