Node.js 비동기 에러 처리에서 unhandledRejection 이벤트 활용하기

문제 상황

프로덕션에서 일부 API 요청이 간헐적으로 실패하는데 로그가 남지 않는 이슈가 발생했다. 원인을 추적해보니 Promise rejection을 catch하지 않은 부분이 있었다.

app.post('/api/users', async (req, res) => {
  const user = await createUser(req.body); // 여기서 에러 발생
  res.json(user);
});

위 코드에서 createUser가 reject되면 Express가 제대로 처리하지 못했다. Node.js 8에서는 unhandled rejection이 경고만 출력되고 프로세스가 계속 실행된다.

해결 방법

전역 이벤트 핸들러를 등록해 모든 unhandled rejection을 캐치하도록 했다.

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  // 로깅 서비스로 전송
  logger.error('Unhandled Promise Rejection', { reason, promise });
});

Express 라우터에는 async 에러를 처리하는 wrapper를 추가했다.

const asyncHandler = fn => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.post('/api/users', asyncHandler(async (req, res) => {
  const user = await createUser(req.body);
  res.json(user);
}));

결과

이후 Sentry에 unhandled rejection 에러가 제대로 리포팅되기 시작했고, 놓치고 있던 버그 3건을 발견해 수정했다. Node.js 향후 버전에서는 unhandled rejection이 프로세스를 종료시킬 예정이라고 하니 미리 대응해두는 것이 좋겠다.